""" Extended tests for news/utils/topic_generator.py Tests cover: - generate_topics() function - _generate_with_llm() helper - _validate_topics() helper - Topic cleaning and validation - Edge cases and error handling """ from unittest.mock import Mock, patch class TestGenerateTopicsBasic: """Basic tests for generate_topics() function.""" def test_returns_list(self): """generate_topics returns a list.""" with patch( "local_deep_research.news.utils.topic_generator._generate_with_llm" ) as mock_gen: mock_gen.return_value = ["Topic 1", "Topic 2"] from local_deep_research.news.utils.topic_generator import ( generate_topics, ) result = generate_topics("test query") assert isinstance(result, list) def test_uses_llm_generated_topics(self): """Uses LLM-generated topics when available.""" with patch( "local_deep_research.news.utils.topic_generator._generate_with_llm" ) as mock_gen: mock_gen.return_value = ["AI", "Technology", "Research"] from local_deep_research.news.utils.topic_generator import ( generate_topics, ) result = generate_topics("query", "findings") # Topics get lowercased by _validate_topics assert "ai" in result or "AI" in result def test_returns_failure_message_when_llm_fails(self): """Returns failure message when LLM fails.""" with patch( "local_deep_research.news.utils.topic_generator._generate_with_llm" ) as mock_gen: mock_gen.return_value = [] from local_deep_research.news.utils.topic_generator import ( generate_topics, ) result = generate_topics("query", "findings") assert any( "failed" in t.lower() or "no valid" in t.lower() for t in result ) def test_accepts_category_parameter(self): """Accepts category parameter.""" with patch( "local_deep_research.news.utils.topic_generator._generate_with_llm" ) as mock_gen: mock_gen.return_value = ["Tech News"] from local_deep_research.news.utils.topic_generator import ( generate_topics, ) generate_topics("query", "findings", category="technology") mock_gen.assert_called_once() def test_respects_max_topics_parameter(self): """Respects max_topics parameter.""" with patch( "local_deep_research.news.utils.topic_generator._generate_with_llm" ) as mock_gen: mock_gen.return_value = ["AA", "BB", "CC", "DD", "EE", "FF"] from local_deep_research.news.utils.topic_generator import ( generate_topics, ) result = generate_topics("query", max_topics=3) assert len(result) <= 3 class TestGenerateWithLLMFunction: """Tests for _generate_with_llm() helper function.""" def test_function_exists(self): """_generate_with_llm function exists.""" from local_deep_research.news.utils.topic_generator import ( _generate_with_llm, ) assert callable(_generate_with_llm) def test_returns_list(self): """_generate_with_llm returns a list.""" with patch( "local_deep_research.config.llm_config.get_llm" ) as mock_get_llm: mock_llm = Mock() mock_llm.invoke.return_value = Mock( content='["Topic 1", "Topic 2"]' ) mock_get_llm.return_value = mock_llm from local_deep_research.news.utils.topic_generator import ( _generate_with_llm, ) result = _generate_with_llm("query", "findings", "", 5) # May return list or empty list depending on LLM response assert isinstance(result, list) def test_returns_empty_on_error(self): """_generate_with_llm returns empty list on error.""" with patch( "local_deep_research.config.llm_config.get_llm" ) as mock_get_llm: mock_get_llm.side_effect = Exception("LLM error") from local_deep_research.news.utils.topic_generator import ( _generate_with_llm, ) result = _generate_with_llm("query", "findings", "", 5) assert result == [] class TestValidateTopics: """Tests for _validate_topics() helper.""" def test_removes_empty_topics(self): """Removes empty string topics.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) result = _validate_topics(["Topic", "", " ", "Valid"], 5) assert "" not in result assert " " not in result def test_removes_short_topics(self): """Removes topics shorter than 2 characters.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) result = _validate_topics(["AI", "A", "AB", "ABC"], 5) assert "a" not in result # Single char removed assert "ab" in result # 2 chars kept assert "abc" in result # 3 chars kept def test_removes_long_topics(self): """Removes topics longer than 30 characters.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) long_topic = "A" * 35 result = _validate_topics(["Short", long_topic], 5) assert "short" in result assert long_topic.lower() not in result def test_removes_duplicates_case_insensitive(self): """Removes duplicate topics (case-insensitive).""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) result = _validate_topics(["AI", "ai", "Ai", "Technology"], 5) assert result.count("ai") == 1 def test_converts_to_lowercase(self): """Converts topics to lowercase.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) result = _validate_topics(["TECHNOLOGY", "Science"], 5) assert "technology" in result assert "science" in result assert "TECHNOLOGY" not in result def test_respects_max_topics(self): """Respects max_topics limit.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) result = _validate_topics( ["A1", "B2", "C3", "D4", "E5", "F6", "G7"], max_topics=3 ) assert len(result) == 3 def test_returns_no_valid_topics_message(self): """Returns failure message when no valid topics.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) result = _validate_topics(["", " ", "A"], 5) # All invalid assert "[No valid topics]" in result class TestTopicGeneratorEdgeCases: """Edge case tests for topic generation.""" def test_empty_query(self): """Handles empty query.""" with patch( "local_deep_research.news.utils.topic_generator._generate_with_llm" ) as mock_gen: mock_gen.return_value = [] from local_deep_research.news.utils.topic_generator import ( generate_topics, ) result = generate_topics("") assert isinstance(result, list) def test_unicode_topics(self): """Handles unicode topics.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) result = _validate_topics(["日本", "économie", "科技"], 5) assert "日本" in result assert "économie" in result def test_topics_with_special_characters(self): """Handles topics with special characters.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) result = _validate_topics(["AI-ML", "COVID-19", "C++"], 5) # Topics are cleaned and lowercased assert any("ai" in t or "ml" in t for t in result) or len(result) > 0 def test_max_topics_zero(self): """Handles max_topics of zero.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) result = _validate_topics(["Topic 1", "Topic 2"], 0) # Should return no valid topics message or empty behavior assert len(result) <= 1 class TestTopicGeneratorIntegration: """Integration tests for topic generation.""" def test_full_flow_with_llm_mock(self): """Full flow with mocked LLM generation.""" with patch( "local_deep_research.news.utils.topic_generator._generate_with_llm" ) as mock_gen: mock_gen.return_value = [ "Artificial Intelligence", "Machine Learning", "Technology", ] from local_deep_research.news.utils.topic_generator import ( generate_topics, ) result = generate_topics( "AI research news", "Recent developments in AI and machine learning...", category="technology", max_topics=5, ) assert len(result) > 0 assert "artificial intelligence" in result def test_full_flow_with_llm_failure(self): """Full flow when LLM fails.""" with patch( "local_deep_research.news.utils.topic_generator._generate_with_llm" ) as mock_gen: mock_gen.return_value = [] # LLM failed, returns empty from local_deep_research.news.utils.topic_generator import ( generate_topics, ) result = generate_topics("query", "findings") # Should return failure message - either "failed" or "no valid" assert isinstance(result, list) assert len(result) > 0 has_failure_indicator = any( "failed" in t.lower() or "no valid" in t.lower() or "[" in t for t in result ) assert has_failure_indicator class TestTopicValidationDetails: """Detailed tests for topic validation.""" def test_strips_whitespace(self): """Strips whitespace from topics.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) result = _validate_topics([" Topic ", "\tAnother\n"], 5) assert "topic" in result assert "another" in result def test_preserves_order(self): """Preserves order of topics.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) result = _validate_topics(["First", "Second", "Third"], 5) assert result[0] == "first" assert result[1] == "second" assert result[2] == "third" def test_handles_only_invalid_topics(self): """Handles list with only invalid topics.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) result = _validate_topics(["", " ", "X"], 5) # All invalid - should get failure message assert "[No valid topics]" in result def test_exact_max_topics_boundary(self): """Handles exactly max_topics number of topics.""" from local_deep_research.news.utils.topic_generator import ( _validate_topics, ) result = _validate_topics(["AA", "BB", "CC"], 3) assert len(result) == 3