"""Unit tests for BearResearcher agent. This module tests the BearResearcher class including: - BearReport generation - Risk factor extraction - Counter-argument generation - Decision cost deduction - Conviction level calculation """ import asyncio from typing import Any, Dict from unittest.mock import MagicMock import pytest from openclaw.agents.base import ActivityType from openclaw.agents.bear_researcher import BearReport, BearResearcher from openclaw.core.economy import SurvivalStatus class TestBearReport: """Test BearReport dataclass.""" def test_default_creation(self): """Test creating BearReport with default values.""" report = BearReport(symbol="AAPL") assert report.symbol == "AAPL" assert report.risk_factors == [] assert report.counter_arguments == {} assert report.downside_target == 0.0 assert report.conviction_level == 0.0 assert report.summary == "" def test_full_creation(self): """Test creating BearReport with all values.""" report = BearReport( symbol="TSLA", risk_factors=["High volatility", "Competition"], counter_arguments={"Growth": "Slowing"}, downside_target=150.0, conviction_level=0.75, summary="Bearish on TSLA", ) assert report.symbol == "TSLA" assert len(report.risk_factors) == 2 assert report.counter_arguments["Growth"] == "Slowing" assert report.downside_target == 150.0 assert report.conviction_level == 0.75 assert report.summary == "Bearish on TSLA" def test_conviction_level_capped(self): """Test conviction level is capped between 0 and 1.""" # Above 1.0 should be capped report_high = BearReport(symbol="AAPL", conviction_level=1.5) assert report_high.conviction_level == 1.0 # Below 0 should be floored report_low = BearReport(symbol="AAPL", conviction_level=-0.5) assert report_low.conviction_level == 0.0 def test_to_dict(self): """Test conversion to dictionary.""" report = BearReport( symbol="AAPL", risk_factors=["Risk 1"], counter_arguments={"Bull": "Counter"}, downside_target=100.0, conviction_level=0.6, summary="Test summary", ) data = report.to_dict() assert data["symbol"] == "AAPL" assert data["risk_factors"] == ["Risk 1"] assert data["counter_arguments"] == {"Bull": "Counter"} assert data["downside_target"] == 100.0 assert data["conviction_level"] == 0.6 assert data["summary"] == "Test summary" class TestBearResearcherInitialization: """Test BearResearcher initialization.""" def test_default_initialization(self): """Test agent with default parameters.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0) assert agent.agent_id == "bear-1" assert agent.balance == 10000.0 assert agent.skill_level == 0.5 assert agent.decision_cost == 0.15 assert agent._last_report is None def test_custom_initialization(self): """Test agent with custom skill level.""" agent = BearResearcher( agent_id="bear-2", initial_capital=5000.0, skill_level=0.8, ) assert agent.agent_id == "bear-2" assert agent.balance == 5000.0 assert agent.skill_level == 0.8 def test_repr(self): """Test string representation.""" agent = BearResearcher(agent_id="bear-test", initial_capital=10000.0) repr_str = repr(agent) assert "BearResearcher" in repr_str assert "bear-test" in repr_str assert "$0.15" in repr_str or "0.15" in repr_str class TestDecideActivity: """Test decide_activity method.""" def test_bankrupt_agent_rest(self): """Test bankrupt agent can only rest.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0) agent.economic_tracker.balance = 1000.0 # Below bankruptcy threshold result = asyncio.run(agent.decide_activity()) assert result == ActivityType.REST def test_critical_agent_learns(self): """Test critical agent focuses on learning.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0) agent.economic_tracker.balance = 3500.0 # Critical level result = asyncio.run(agent.decide_activity()) assert result == ActivityType.LEARN def test_struggling_agent_paper_trades(self): """Test struggling agent does paper trading.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0) agent.economic_tracker.balance = 8500.0 # Struggling level result = asyncio.run(agent.decide_activity()) assert result == ActivityType.PAPER_TRADE def test_stable_agent_analyzes(self): """Test stable agent analyzes.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0) agent.economic_tracker.balance = 12000.0 # Stable level result = asyncio.run(agent.decide_activity()) assert result == ActivityType.ANALYZE class TestDecisionCost: """Test decision cost deduction.""" def test_decision_cost_deducted_in_analyze(self): """Test that decision cost is deducted during analysis.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0) initial_balance = agent.balance asyncio.run(agent.analyze("AAPL")) # Balance should have decreased assert agent.balance < initial_balance def test_decision_cost_deducted_in_generate_bear_case(self): """Test that decision cost is deducted when generating bear case.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0) initial_balance = agent.balance asyncio.run(agent.generate_bear_case("AAPL")) # Balance should have decreased assert agent.balance < initial_balance def test_cannot_afford_analysis(self): """Test behavior when agent cannot afford analysis.""" # Start with balance below decision cost threshold agent = BearResearcher(agent_id="bear-1", initial_capital=0.10) report = asyncio.run(agent.generate_bear_case("AAPL")) # Should return a report with insufficient funds message assert report.symbol == "AAPL" assert "insufficient" in report.summary.lower() or report.conviction_level == 0.0 def test_decision_cost_constant(self): """Test that decision cost is set to $0.15.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0) assert agent.decision_cost == 0.15 class TestGenerateBearCase: """Test generate_bear_case method.""" def test_generates_bear_report(self): """Test that bear case generation returns BearReport.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0) report = asyncio.run(agent.generate_bear_case("AAPL")) assert isinstance(report, BearReport) assert report.symbol == "AAPL" def test_report_contains_risk_factors(self): """Test that report contains risk factors.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0, skill_level=0.8) report = asyncio.run(agent.generate_bear_case("AAPL")) assert len(report.risk_factors) > 0 def test_report_contains_counter_arguments(self): """Test that report contains counter-arguments.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0) report = asyncio.run(agent.generate_bear_case("AAPL")) assert len(report.counter_arguments) > 0 def test_conviction_level_in_valid_range(self): """Test that conviction level is between 0 and 1.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0) report = asyncio.run(agent.generate_bear_case("AAPL")) assert 0.0 <= report.conviction_level <= 1.0 def test_conviction_based_on_skill(self): """Test that higher skill leads to higher conviction.""" low_skill_agent = BearResearcher( agent_id="bear-low", initial_capital=10000.0, skill_level=0.3 ) high_skill_agent = BearResearcher( agent_id="bear-high", initial_capital=10000.0, skill_level=0.9 ) low_report = asyncio.run(low_skill_agent.generate_bear_case("AAPL")) high_report = asyncio.run(high_skill_agent.generate_bear_case("AAPL")) # Higher skill should generally lead to higher conviction # (may not always be true due to randomness in tests, but should trend this way) assert high_report.conviction_level >= low_report.conviction_level def test_with_technical_report(self): """Test bear case generation with technical report input.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0, skill_level=0.8) # Mock technical report tech_report = {"support_level": 150.0, "rsi": 75} report = asyncio.run(agent.generate_bear_case("AAPL", technical_report=tech_report)) assert report.symbol == "AAPL" assert len(report.risk_factors) > 0 assert len(report.counter_arguments) > 0 def test_with_all_reports(self): """Test bear case generation with all report types.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0, skill_level=0.9) tech_report = {"support_level": 150.0} sentiment_report = {"sentiment": "bullish"} fundamental_report = {"pe_ratio": 30.0} report = asyncio.run( agent.generate_bear_case( "AAPL", technical_report=tech_report, sentiment_report=sentiment_report, fundamental_report=fundamental_report, ) ) assert report.symbol == "AAPL" assert len(report.risk_factors) >= 3 # Should have risks from all reports assert report.conviction_level > 0.4 # Higher conviction with more data def test_last_report_saved(self): """Test that last report is saved.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0) assert agent.get_last_report() is None report = asyncio.run(agent.generate_bear_case("AAPL")) assert agent.get_last_report() is report class TestExtractRiskFactors: """Test risk factor extraction.""" def test_extract_from_technical_report(self): """Test extracting risks from technical report.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0, skill_level=1.0) tech_report = MagicMock() risks = agent._extract_risk_factors(tech_report, None, None) assert len(risks) > 0 # Should contain technical risks assert any("RSI" in risk or "support" in risk.lower() for risk in risks) def test_extract_from_sentiment_report(self): """Test extracting risks from sentiment report.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0, skill_level=1.0) sentiment_report = MagicMock() risks = agent._extract_risk_factors(None, sentiment_report, None) assert len(risks) > 0 def test_extract_from_fundamental_report(self): """Test extracting risks from fundamental report.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0, skill_level=1.0) fundamental_report = MagicMock() risks = agent._extract_risk_factors(None, None, fundamental_report) assert len(risks) > 0 def test_generic_risks_when_no_reports(self): """Test generic risks when no reports provided.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0, skill_level=0.5) risks = agent._extract_risk_factors(None, None, None) assert len(risks) > 0 # Should have generic risks assert any("volatility" in risk.lower() or "uncertainty" in risk.lower() for risk in risks) class TestGenerateCounterArguments: """Test counter-argument generation.""" def test_counters_with_technical(self): """Test counter-arguments with technical report.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0) tech_report = MagicMock() counters = agent._generate_counter_arguments(tech_report, None, None) assert len(counters) > 0 def test_counters_with_fundamental(self): """Test counter-arguments with fundamental report.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0) fundamental_report = MagicMock() counters = agent._generate_counter_arguments(None, None, fundamental_report) assert len(counters) > 0 # Should contain fundamental counters assert any("growth" in k.lower() or "valuation" in k.lower() for k in counters.keys()) def test_generic_counters_when_no_reports(self): """Test generic counter-arguments when no reports.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0) counters = agent._generate_counter_arguments(None, None, None) assert len(counters) > 0 class TestCounterBullishPoint: """Test counter_bullish_point method.""" def test_counter_strong_growth(self): """Test counter to strong growth argument.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0) counter = agent.counter_bullish_point("strong growth") assert "peaking" in counter.lower() or "growth" in counter.lower() def test_counter_undervalued(self): """Test counter to undervalued argument.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0) counter = agent.counter_bullish_point("undervalued") assert "value trap" in counter.lower() or "value" in counter.lower() def test_counter_market_leader(self): """Test counter to market leader argument.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0) counter = agent.counter_bullish_point("market leader") assert "competition" in counter.lower() or "leader" in counter.lower() def test_generic_counter_for_unknown(self): """Test generic counter for unknown bullish point.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0) counter = agent.counter_bullish_point("some random bullish point xyz") # Should return a generic counter-argument assert len(counter) > 0 class TestCalculateConviction: """Test conviction calculation.""" def test_conviction_based_on_skill(self): """Test conviction is influenced by skill level.""" low_skill = BearResearcher(agent_id="bear-low", initial_capital=10000.0, skill_level=0.3) high_skill = BearResearcher(agent_id="bear-high", initial_capital=10000.0, skill_level=0.9) low_conviction = low_skill._calculate_conviction([], None, None, None) high_conviction = high_skill._calculate_conviction([], None, None, None) # Higher skill should generally lead to higher base conviction assert high_conviction > low_conviction def test_conviction_with_more_risks(self): """Test conviction increases with more risk factors.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0, skill_level=0.5) few_risks = ["Risk 1"] many_risks = ["Risk 1", "Risk 2", "Risk 3", "Risk 4"] few_conviction = agent._calculate_conviction(few_risks, None, None, None) many_conviction = agent._calculate_conviction(many_risks, None, None, None) # More risks should generally lead to higher conviction assert many_conviction >= few_conviction def test_conviction_capped(self): """Test conviction is capped at 0.9.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0, skill_level=1.0) # Create many risk factors and reports to maximize conviction many_risks = [f"Risk {i}" for i in range(20)] tech_report = MagicMock() sentiment_report = MagicMock() fundamental_report = MagicMock() conviction = agent._calculate_conviction( many_risks, tech_report, sentiment_report, fundamental_report ) # Should be capped at 0.9 for bearish views assert conviction <= 0.9 class TestGenerateSummary: """Test summary generation.""" def test_summary_based_on_conviction(self): """Test summary tone changes based on conviction.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0) low_summary = agent._generate_summary("AAPL", [], 0.3) medium_summary = agent._generate_summary("AAPL", [], 0.5) high_summary = agent._generate_summary("AAPL", [], 0.8) # Different conviction levels should produce different tones assert "Mildly cautious" in low_summary or "cautious" in low_summary.lower() assert "Bearish" in high_summary or "bearish" in high_summary.lower() def test_summary_includes_symbol(self): """Test summary includes the symbol.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0) summary = agent._generate_summary("TSLA", ["Risk 1", "Risk 2"], 0.6) assert "TSLA" in summary def test_summary_includes_risk_count(self): """Test summary includes risk count.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0) risks = ["Risk 1", "Risk 2", "Risk 3"] summary = agent._generate_summary("AAPL", risks, 0.6) assert "3" in summary or "three" in summary.lower() class TestAnalyzeMethod: """Test the analyze method.""" def test_analyze_returns_dict(self): """Test analyze returns a dictionary.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0) result = asyncio.run(agent.analyze("AAPL")) assert isinstance(result, dict) assert result["symbol"] == "AAPL" def test_analyze_deducts_cost(self): """Test analyze deducts decision cost.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0) initial_balance = agent.balance asyncio.run(agent.analyze("AAPL")) assert agent.balance < initial_balance def test_analyze_contains_bear_report(self): """Test analyze result contains bear report.""" agent = BearResearcher(agent_id="bear-1", initial_capital=10000.0) result = asyncio.run(agent.analyze("AAPL")) assert "bear_report" in result assert result["bear_report"]["symbol"] == "AAPL"