""" Deep behavioral tests for NewsSearchCallback and search integration. Tests quality calculation, tracking control, and search wrapper behavior. """ from unittest.mock import Mock from local_deep_research.news.core.search_integration import ( NewsSearchCallback, create_search_wrapper, ) # --- NewsSearchCallback init --- class TestNewsSearchCallbackInit: """Tests for NewsSearchCallback initialization.""" def test_tracking_initially_none(self): cb = NewsSearchCallback() assert cb._tracking_enabled is None def test_tracking_defaults_to_false(self): cb = NewsSearchCallback() assert cb.tracking_enabled is False def test_tracking_cached_after_first_access(self): cb = NewsSearchCallback() _ = cb.tracking_enabled assert cb._tracking_enabled is not None # --- _calculate_quality --- class TestCalculateQuality: """Tests for search result quality calculation.""" def test_empty_findings_returns_zero(self): cb = NewsSearchCallback() result = {"findings": []} assert cb._calculate_quality(result) == 0.0 def test_no_findings_key_returns_zero(self): cb = NewsSearchCallback() assert cb._calculate_quality({}) == 0.0 def test_single_finding_with_content(self): cb = NewsSearchCallback() result = {"findings": [{"content": "Some content"}]} score = cb._calculate_quality(result) # count_score = min(1/10, 1.0) = 0.1 # content_score = 1.0 (has content) # (0.1 + 1.0) / 2 = 0.55 assert abs(score - 0.55) < 0.01 def test_single_finding_without_content(self): cb = NewsSearchCallback() result = {"findings": [{"title": "No content field"}]} score = cb._calculate_quality(result) # count_score = 0.1 # content_score = 0.5 (no content) # (0.1 + 0.5) / 2 = 0.3 assert abs(score - 0.3) < 0.01 def test_ten_findings_maxes_count_score(self): cb = NewsSearchCallback() findings = [{"content": f"content {i}"} for i in range(10)] result = {"findings": findings} score = cb._calculate_quality(result) # count_score = min(10/10, 1.0) = 1.0 # content_score = 1.0 # (1.0 + 1.0) / 2 = 1.0 assert score == 1.0 def test_twenty_findings_still_capped(self): cb = NewsSearchCallback() findings = [{"content": f"content {i}"} for i in range(20)] result = {"findings": findings} score = cb._calculate_quality(result) # count_score = min(20/10, 1.0) = 1.0 (capped) assert score == 1.0 def test_five_findings_with_content(self): cb = NewsSearchCallback() findings = [{"content": f"content {i}"} for i in range(5)] result = {"findings": findings} score = cb._calculate_quality(result) # count_score = 0.5, content_score = 1.0 # (0.5 + 1.0) / 2 = 0.75 assert abs(score - 0.75) < 0.01 def test_five_findings_without_content(self): cb = NewsSearchCallback() findings = [{"title": f"title {i}"} for i in range(5)] result = {"findings": findings} score = cb._calculate_quality(result) # count_score = 0.5, content_score = 0.5 # (0.5 + 0.5) / 2 = 0.5 assert abs(score - 0.5) < 0.01 def test_content_check_only_first_five(self): """Content check looks at only the first 5 findings.""" cb = NewsSearchCallback() # First 5 have no content, rest do findings = [{"title": f"t {i}"} for i in range(5)] findings += [{"content": f"c {i}"} for i in range(5)] result = {"findings": findings} score = cb._calculate_quality(result) # count_score = min(10/10, 1.0) = 1.0 # content_score = 0.5 (first 5 have no content) # (1.0 + 0.5) / 2 = 0.75 assert abs(score - 0.75) < 0.01 def test_content_in_first_finding_counts(self): cb = NewsSearchCallback() findings = [ {"content": "has content"}, {"title": "no content"}, {"title": "no content"}, ] result = {"findings": findings} score = cb._calculate_quality(result) # count_score = 0.3, content_score = 1.0 # (0.3 + 1.0) / 2 = 0.65 assert abs(score - 0.65) < 0.01 def test_empty_content_is_falsy(self): cb = NewsSearchCallback() findings = [{"content": ""}, {"content": ""}] result = {"findings": findings} score = cb._calculate_quality(result) # Empty strings are falsy # count_score = 0.2, content_score = 0.5 assert score < 0.5 def test_none_content_is_falsy(self): cb = NewsSearchCallback() findings = [{"content": None}] result = {"findings": findings} score = cb._calculate_quality(result) # None is falsy # count_score = 0.1, content_score = 0.5 assert abs(score - 0.3) < 0.01 # --- __call__ --- class TestNewsSearchCallbackCall: """Tests for the callback invocation.""" def test_does_not_track_when_disabled(self): cb = NewsSearchCallback() cb._tracking_enabled = False # Should not raise even without tracking infrastructure cb("test query", {"findings": []}) def test_does_not_track_non_user_search(self): cb = NewsSearchCallback() cb._tracking_enabled = True context = {"is_user_search": False} # Should not attempt tracking for non-user searches cb("test query", {"findings": []}, context) def test_handles_no_context(self): cb = NewsSearchCallback() cb._tracking_enabled = False cb("test query", {"findings": []}, None) def test_handles_empty_context(self): cb = NewsSearchCallback() cb._tracking_enabled = False cb("test query", {"findings": []}, {}) # --- create_search_wrapper --- class TestCreateSearchWrapper: """Tests for search method wrapping.""" def test_wrapper_is_callable(self): def original(self, query, **kwargs): return {"findings": []} wrapped = create_search_wrapper(original) assert callable(wrapped) def test_preserves_function_name(self): def my_search(self, query, **kwargs): return {} wrapped = create_search_wrapper(my_search) assert wrapped.__name__ == "my_search" def test_calls_original_method(self): call_tracker = {"called": False} def original(self, query, **kwargs): call_tracker["called"] = True return {"findings": []} wrapped = create_search_wrapper(original) wrapped(Mock(), "test query") assert call_tracker["called"] def test_returns_original_result(self): expected = {"findings": ["result1"]} def original(self, query, **kwargs): return expected wrapped = create_search_wrapper(original) result = wrapped(Mock(), "test query") assert result == expected def test_strips_news_kwargs(self): """News-specific kwargs should be stripped before passing to original.""" received_kwargs = {} def original(self, query, **kwargs): received_kwargs.update(kwargs) return {} wrapped = create_search_wrapper(original) wrapped( Mock(), "test", is_user_search=True, is_news_search=False, user_id="u1", ) assert "is_user_search" not in received_kwargs assert "is_news_search" not in received_kwargs assert "user_id" not in received_kwargs def test_callback_error_does_not_propagate(self): """If callback fails, the search result should still be returned.""" def original(self, query, **kwargs): return {"findings": []} wrapped = create_search_wrapper(original) result = wrapped(Mock(), "test query") assert result is not None