"""Tests for summarize helpers.""" import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent / "scripts")) from datetime import datetime import summarize from summarize import ( MoverContext, SectorCluster, WatchpointsData, build_watchpoints_data, classify_move_type, detect_sector_clusters, format_watchpoints, get_index_change, match_headline_to_symbol, ) class FixedDateTime(datetime): @classmethod def now(cls, tz=None): return cls(2026, 1, 1, 15, 0) def test_generate_briefing_auto_time_evening(capsys, monkeypatch): def fake_market_news(*_args, **_kwargs): return { "headlines": [ {"source": "CNBC", "title": "Headline one", "link": "https://example.com/1"}, {"source": "Yahoo", "title": "Headline two", "link": "https://example.com/2"}, {"source": "CNBC", "title": "Headline three", "link": "https://example.com/3"}, ], "markets": { "us": { "name": "US Markets", "indices": { "^GSPC": {"name": "S&P 500", "data": {"price": 100, "change_percent": 1.0}}, }, } }, } def fake_summary(*_args, **_kwargs): return "OK" monkeypatch.setattr(summarize, "get_market_news", fake_market_news) monkeypatch.setattr(summarize, "get_portfolio_news", lambda *_a, **_k: None) monkeypatch.setattr(summarize, "summarize_with_claude", fake_summary) monkeypatch.setattr(summarize, "datetime", FixedDateTime) args = type( "Args", (), { "lang": "de", "style": "briefing", "time": None, "model": "claude", "json": False, "research": False, "deadline": None, "fast": False, "llm": False, "debug": False, }, )() summarize.generate_briefing(args) stdout = capsys.readouterr().out assert "Börsen Abend-Briefing" in stdout # --- Tests for watchpoints feature (Issue #92) --- class TestGetIndexChange: def test_extracts_sp500_change(self): market_data = { "markets": { "us": { "indices": { "^GSPC": {"data": {"change_percent": -1.5}} } } } } assert get_index_change(market_data) == -1.5 def test_returns_zero_on_missing_data(self): assert get_index_change({}) == 0.0 assert get_index_change({"markets": {}}) == 0.0 assert get_index_change({"markets": {"us": {}}}) == 0.0 class TestMatchHeadlineToSymbol: def test_exact_symbol_match_dollar(self): headlines = [{"title": "Breaking: $NVDA surges on AI demand"}] result = match_headline_to_symbol("NVDA", "NVIDIA Corporation", headlines) assert result is not None assert "NVDA" in result["title"] def test_exact_symbol_match_parens(self): headlines = [{"title": "Tesla (TSLA) reports record deliveries"}] result = match_headline_to_symbol("TSLA", "Tesla Inc", headlines) assert result is not None def test_exact_symbol_match_word_boundary(self): headlines = [{"title": "AAPL announces new product line"}] result = match_headline_to_symbol("AAPL", "Apple Inc", headlines) assert result is not None def test_company_name_match(self): headlines = [{"title": "Apple announces record iPhone sales"}] result = match_headline_to_symbol("AAPL", "Apple Inc", headlines) assert result is not None def test_no_match_returns_none(self): headlines = [{"title": "Fed raises interest rates"}] result = match_headline_to_symbol("NVDA", "NVIDIA Corporation", headlines) assert result is None def test_avoids_partial_symbol_match(self): # "APP" should not match "application" headlines = [{"title": "New application launches today"}] result = match_headline_to_symbol("APP", "AppLovin Corp", headlines) assert result is None def test_empty_headlines(self): result = match_headline_to_symbol("NVDA", "NVIDIA", []) assert result is None class TestDetectSectorClusters: def test_detects_cluster_three_stocks_same_direction(self): movers = [ {"symbol": "NVDA", "change_pct": -5.0}, {"symbol": "AMD", "change_pct": -4.0}, {"symbol": "INTC", "change_pct": -3.0}, ] portfolio_meta = { "NVDA": {"category": "Tech"}, "AMD": {"category": "Tech"}, "INTC": {"category": "Tech"}, } clusters = detect_sector_clusters(movers, portfolio_meta) assert len(clusters) == 1 assert clusters[0].category == "Tech" assert clusters[0].direction == "down" assert len(clusters[0].stocks) == 3 def test_no_cluster_if_less_than_three(self): movers = [ {"symbol": "NVDA", "change_pct": -5.0}, {"symbol": "AMD", "change_pct": -4.0}, ] portfolio_meta = { "NVDA": {"category": "Tech"}, "AMD": {"category": "Tech"}, } clusters = detect_sector_clusters(movers, portfolio_meta) assert len(clusters) == 0 def test_no_cluster_if_mixed_direction(self): movers = [ {"symbol": "NVDA", "change_pct": 5.0}, {"symbol": "AMD", "change_pct": -4.0}, {"symbol": "INTC", "change_pct": 3.0}, ] portfolio_meta = { "NVDA": {"category": "Tech"}, "AMD": {"category": "Tech"}, "INTC": {"category": "Tech"}, } clusters = detect_sector_clusters(movers, portfolio_meta) assert len(clusters) == 0 class TestClassifyMoveType: def test_earnings_with_keyword(self): headline = {"title": "Company beats Q3 earnings expectations"} result = classify_move_type(headline, False, 5.0, 0.1) assert result == "earnings" def test_sector_cluster(self): result = classify_move_type(None, True, -3.0, -0.5) assert result == "sector" def test_market_wide(self): result = classify_move_type(None, False, -2.0, -2.0) assert result == "market_wide" def test_company_specific_with_headline(self): headline = {"title": "Company announces acquisition"} result = classify_move_type(headline, False, 3.0, 0.1) assert result == "company_specific" def test_company_specific_large_move_no_headline(self): result = classify_move_type(None, False, 8.0, 0.1) assert result == "company_specific" def test_unknown_small_move_no_context(self): result = classify_move_type(None, False, 1.5, 0.2) assert result == "unknown" class TestFormatWatchpoints: def test_formats_sector_cluster(self): cluster = SectorCluster( category="Tech", stocks=[ MoverContext("NVDA", -5.0, 100.0, "Tech", None, "sector", None), MoverContext("AMD", -4.0, 80.0, "Tech", None, "sector", None), MoverContext("INTC", -3.0, 30.0, "Tech", None, "sector", None), ], avg_change=-4.0, direction="down", vs_index=-3.5, ) data = WatchpointsData( movers=[], sector_clusters=[cluster], index_change=-0.5, market_wide=False, ) result = format_watchpoints(data, "en", {}) assert "Tech" in result assert "-4.0%" in result assert "vs Index" in result def test_formats_individual_mover_with_headline(self): mover = MoverContext( symbol="NVDA", change_pct=5.0, price=100.0, category="Tech", matched_headline={"title": "NVIDIA reports record revenue"}, move_type="company_specific", vs_index=4.5, ) data = WatchpointsData( movers=[mover], sector_clusters=[], index_change=0.5, market_wide=False, ) result = format_watchpoints(data, "en", {}) assert "NVDA" in result assert "+5.0%" in result assert "record revenue" in result def test_formats_market_wide_move_english(self): data = WatchpointsData( movers=[], sector_clusters=[], index_change=-2.0, market_wide=True, ) result = format_watchpoints(data, "en", {}) assert "Market-wide move" in result assert "S&P 500 fell 2.0%" in result def test_formats_market_wide_move_german(self): data = WatchpointsData( movers=[], sector_clusters=[], index_change=2.5, market_wide=True, ) result = format_watchpoints(data, "de", {}) assert "Breite Marktbewegung" in result assert "stieg 2.5%" in result def test_uses_label_fallbacks(self): mover = MoverContext( symbol="XYZ", change_pct=1.5, price=50.0, category="Other", matched_headline=None, move_type="unknown", vs_index=1.0, ) data = WatchpointsData( movers=[mover], sector_clusters=[], index_change=0.5, market_wide=False, ) labels = {"no_catalyst": " -- no news"} result = format_watchpoints(data, "en", labels) assert "XYZ" in result assert "no news" in result class TestBuildWatchpointsData: def test_builds_complete_data_structure(self): movers = [ {"symbol": "NVDA", "change_pct": -5.0, "price": 100.0}, {"symbol": "AMD", "change_pct": -4.0, "price": 80.0}, {"symbol": "INTC", "change_pct": -3.0, "price": 30.0}, {"symbol": "AAPL", "change_pct": 2.0, "price": 150.0}, ] headlines = [ {"title": "NVIDIA reports weak guidance"}, {"title": "Apple announces new product"}, ] portfolio_meta = { "NVDA": {"category": "Tech", "name": "NVIDIA Corporation"}, "AMD": {"category": "Tech", "name": "Advanced Micro Devices"}, "INTC": {"category": "Tech", "name": "Intel Corporation"}, "AAPL": {"category": "Tech", "name": "Apple Inc"}, } index_change = -0.5 result = build_watchpoints_data(movers, headlines, portfolio_meta, index_change) # Should detect Tech sector cluster (3 losers) assert len(result.sector_clusters) == 1 assert result.sector_clusters[0].category == "Tech" assert result.sector_clusters[0].direction == "down" # All movers should be present assert len(result.movers) == 4 # NVDA should have matched headline nvda_mover = next(m for m in result.movers if m.symbol == "NVDA") assert nvda_mover.matched_headline is not None assert "guidance" in nvda_mover.matched_headline["title"] # vs_index should be calculated assert nvda_mover.vs_index == -5.0 - (-0.5) # -4.5 def test_handles_empty_movers(self): result = build_watchpoints_data([], [], {}, 0.0) assert result.movers == [] assert result.sector_clusters == [] assert result.market_wide is False def test_detects_market_wide_move(self): result = build_watchpoints_data([], [], {}, -2.0) assert result.market_wide is True