""" Comprehensive tests for topic_generator module. Tests topic extraction with LLM integration, validation, and edge cases. """ from unittest.mock import patch class TestGenerateTopics: """Tests for generate_topics function.""" @patch("local_deep_research.news.utils.topic_generator._generate_with_llm") @patch("local_deep_research.news.utils.topic_generator._validate_topics") def test_returns_validated_topics(self, mock_validate, mock_llm_gen): """Test returns validated topics from LLM.""" from local_deep_research.news.utils.topic_generator import ( generate_topics, ) mock_llm_gen.return_value = ["AI", "Technology", "Research"] mock_validate.return_value = ["ai", "technology", "research"] result = generate_topics("AI news", findings="Content") assert result == ["ai", "technology", "research"] @patch("local_deep_research.news.utils.topic_generator._generate_with_llm") @patch("local_deep_research.news.utils.topic_generator._validate_topics") def test_returns_failure_marker_when_llm_fails( self, mock_validate, mock_llm_gen ): """Test returns failure marker when LLM returns empty.""" from local_deep_research.news.utils.topic_generator import ( generate_topics, ) mock_llm_gen.return_value = [] mock_validate.return_value = ["[topic generation failed]"] _ = generate_topics("query", findings="data") mock_validate.assert_called_once() call_args = mock_validate.call_args[0] assert call_args[0] == ["[Topic generation failed]"] @patch("local_deep_research.news.utils.topic_generator._generate_with_llm") @patch("local_deep_research.news.utils.topic_generator._validate_topics") def test_passes_max_topics_to_validator(self, mock_validate, mock_llm_gen): """Test passes max_topics to validator.""" from local_deep_research.news.utils.topic_generator import ( generate_topics, ) mock_llm_gen.return_value = ["A", "B"] mock_validate.return_value = ["a", "b"] generate_topics("query", max_topics=3) mock_validate.assert_called_once() assert mock_validate.call_args[0][1] == 3 @patch("local_deep_research.news.utils.topic_generator._generate_with_llm") @patch("local_deep_research.news.utils.topic_generator._validate_topics") def test_default_max_topics_is_5(self, mock_validate, mock_llm_gen): """Test default max_topics is 5.""" from local_deep_research.news.utils.topic_generator import ( generate_topics, ) mock_llm_gen.return_value = [] mock_validate.return_value = [] generate_topics("query") mock_llm_gen.assert_called_once() # Fourth argument is max_topics assert mock_llm_gen.call_args[0][3] == 5 @patch("local_deep_research.news.utils.topic_generator._generate_with_llm") @patch("local_deep_research.news.utils.topic_generator._validate_topics") def test_passes_category_to_llm(self, mock_validate, mock_llm_gen): """Test passes category to LLM generator.""" from local_deep_research.news.utils.topic_generator import ( generate_topics, ) mock_llm_gen.return_value = [] mock_validate.return_value = [] generate_topics("query", category="Technology") assert mock_llm_gen.call_args[0][2] == "Technology" class TestGenerateWithLLMTopicsBehavior: """Tests for _generate_with_llm behavior.""" def test_function_exists_and_is_callable(self): """Test _generate_with_llm function exists and is callable.""" from local_deep_research.news.utils.topic_generator import ( _generate_with_llm, ) assert callable(_generate_with_llm) def test_accepts_four_parameters(self): """Test accepts query, findings, category, and max_topics parameters.""" from local_deep_research.news.utils.topic_generator import ( _generate_with_llm, ) import inspect sig = inspect.signature(_generate_with_llm) params = list(sig.parameters.keys()) # Now accepts settings_snapshot for egress-policy threading. assert len(params) == 5 assert "settings_snapshot" in params assert "query" in params assert "findings" in params assert "category" in params assert "max_topics" in params def test_returns_list(self): """Test returns a list (empty or with topics).""" from local_deep_research.news.utils.topic_generator import ( _generate_with_llm, ) # Without proper LLM setup, may return empty list result = _generate_with_llm("query", "", "", 5) assert isinstance(result, list) class TestValidateTopics: """Tests for _validate_topics function.""" def test_returns_list(self): """Test returns a list.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) result = _validate_topics(["AI", "Tech"], 5) assert isinstance(result, list) def test_removes_empty_topics(self): """Test removes empty string topics.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) result = _validate_topics(["AI", "", "Tech", ""], 5) assert "" not in result def test_removes_whitespace_only_topics(self): """Test removes whitespace-only topics.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) result = _validate_topics(["AI", " ", "Tech"], 5) assert len([t for t in result if t.strip() == ""]) == 0 def test_removes_too_short_topics(self): """Test removes topics shorter than 2 characters.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) result = _validate_topics(["A", "AI", "Tech"], 5) assert "A" not in result or len("A") >= 2 # Single char removed def test_removes_too_long_topics(self): """Test removes topics longer than 30 characters.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) long_topic = "A" * 35 result = _validate_topics(["AI", long_topic, "Tech"], 5) assert long_topic not in result def test_removes_duplicates_case_insensitive(self): """Test removes duplicates case-insensitively.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) result = _validate_topics(["AI", "ai", "Ai"], 5) assert len(result) == 1 def test_limits_to_max_topics(self): """Test limits output to max_topics.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) topics = ["AI", "Tech", "Science", "News", "Research", "Data"] result = _validate_topics(topics, 3) assert len(result) <= 3 def test_converts_to_lowercase(self): """Test converts topics to lowercase.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) result = _validate_topics(["UPPERCASE", "MixedCase"], 5) assert all(t == t.lower() for t in result if t != "[No valid topics]") def test_returns_no_valid_marker_when_empty(self): """Test returns marker when no valid topics.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) result = _validate_topics([], 5) assert result == ["[No valid topics]"] def test_returns_no_valid_marker_when_all_invalid(self): """Test returns marker when all topics are invalid.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) result = _validate_topics(["", "A", "B" * 50], 5) assert result == ["[No valid topics]"] class TestTopicGeneratorEdgeCases: """Edge case tests for topic generator.""" @patch("local_deep_research.news.utils.topic_generator._generate_with_llm") @patch("local_deep_research.news.utils.topic_generator._validate_topics") def test_handles_unicode_topics(self, mock_validate, mock_llm_gen): """Test handles unicode characters in topics.""" from local_deep_research.news.utils.topic_generator import ( generate_topics, ) mock_llm_gen.return_value = ["日本", "テクノロジー"] mock_validate.return_value = ["日本", "テクノロジー"] result = generate_topics("Japanese tech news") assert result == ["日本", "テクノロジー"] def test_validate_topics_handles_mixed_types(self): """Test _validate_topics handles mixed types.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) # Validate handles valid strings result = _validate_topics(["AI", "Tech", "Science"], 5) assert isinstance(result, list) assert all(isinstance(t, str) for t in result) def test_generate_topics_returns_list_for_empty_input(self): """Test generate_topics returns list for empty input.""" from local_deep_research.news.utils.topic_generator import ( generate_topics, ) # With empty inputs, should return failure marker result = generate_topics("") assert isinstance(result, list) def test_validate_topics_handles_none_input(self): """Test _validate_topics handles None input gracefully.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) # If topics is None, function should handle it try: _ = _validate_topics(None, 5) # Should return failure marker or handle gracefully except (TypeError, AttributeError): # This is acceptable - None input may raise pass @patch("local_deep_research.news.utils.topic_generator._generate_with_llm") @patch("local_deep_research.news.utils.topic_generator._validate_topics") def test_zero_max_topics(self, mock_validate, mock_llm_gen): """Test handles zero max_topics.""" from local_deep_research.news.utils.topic_generator import ( generate_topics, ) mock_llm_gen.return_value = [] mock_validate.return_value = [] result = generate_topics("query", max_topics=0) assert isinstance(result, list) class TestTopicGeneratorImports: """Tests for module imports.""" def test_imports_generate_topics(self): """Test generate_topics can be imported.""" from local_deep_research.news.utils.topic_generator import ( generate_topics, ) assert generate_topics is not None assert callable(generate_topics) def test_imports_validate_topics(self): """Test _validate_topics can be imported.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) assert _validate_topics is not None assert callable(_validate_topics) def test_imports_generate_with_llm(self): """Test _generate_with_llm can be imported.""" from local_deep_research.news.utils.topic_generator import ( _generate_with_llm, ) assert _generate_with_llm is not None assert callable(_generate_with_llm)