""" Tests for report_generator.py - Section Generation and State Management Tests cover the _research_and_generate_sections() method which: - Initializes questions from previous iterations - Manages search system state between subsections - Restores max_iterations after errors - Creates default subsections when none provided """ from unittest.mock import MagicMock, patch import pytest class TestSectionGenerationStateManagement: """Tests for state management during section generation.""" @pytest.fixture def report_generator(self): """Create a report generator with mocked dependencies.""" with patch("local_deep_research.report_generator.AdvancedSearchSystem"): with patch( "local_deep_research.report_generator.get_llm" ) as mock_get_llm: mock_llm = MagicMock() mock_get_llm.return_value = mock_llm # Create a mock search system with necessary attributes mock_search_system = MagicMock() mock_search_system.all_links_of_system = [] mock_search_system.max_iterations = 3 mock_search_system.questions_by_iteration = {} mock_search_system.analyze_topic.return_value = { "current_knowledge": "Generated content for section" } # Create a mock strategy mock_strategy = MagicMock() mock_strategy.questions_by_iteration = {} mock_strategy.settings_snapshot = {"search.iterations": 3} mock_strategy.max_iterations = 3 mock_search_system.strategy = mock_strategy from local_deep_research.report_generator import ( IntegratedReportGenerator, ) generator = IntegratedReportGenerator(llm=mock_llm) generator.search_system = mock_search_system yield generator def test_questions_preserved_from_initial_findings(self, report_generator): """Questions from initial research should be passed to search system.""" initial_findings = { "current_knowledge": "test content", "questions_by_iteration": { 0: ["Q1: What is the topic?", "Q2: How does it work?"], 1: ["Q3: What are the applications?"], }, } structure = [ { "name": "Introduction", "subsections": [ {"name": "Overview", "purpose": "Intro purpose"} ], } ] report_generator._research_and_generate_sections( initial_findings, structure, "test query" ) # Verify questions were copied to strategy assert ( report_generator.search_system.strategy.questions_by_iteration == initial_findings["questions_by_iteration"] ) def test_empty_questions_handled_gracefully(self, report_generator): """Empty questions_by_iteration should not cause errors.""" initial_findings = { "current_knowledge": "test content", "questions_by_iteration": {}, } structure = [ { "name": "Test Section", "subsections": [{"name": "Sub", "purpose": "Purpose"}], } ] # Should not raise any exception sections = report_generator._research_and_generate_sections( initial_findings, structure, "test query" ) assert "Test Section" in sections def test_missing_questions_key_handled(self, report_generator): """Missing questions_by_iteration key should not cause errors.""" initial_findings = {"current_knowledge": "test content"} structure = [ { "name": "Test Section", "subsections": [{"name": "Sub", "purpose": "Purpose"}], } ] # Should not raise any exception sections = report_generator._research_and_generate_sections( initial_findings, structure, "test query" ) assert "Test Section" in sections class TestSectionGenerationEmptySubsections: """Tests for handling sections without subsections.""" @pytest.fixture def report_generator(self): """Create a report generator with mocked dependencies.""" with patch("local_deep_research.report_generator.AdvancedSearchSystem"): with patch( "local_deep_research.report_generator.get_llm" ) as mock_get_llm: mock_llm = MagicMock() mock_get_llm.return_value = mock_llm mock_search_system = MagicMock() mock_search_system.all_links_of_system = [] mock_search_system.max_iterations = 3 mock_search_system.analyze_topic.return_value = { "current_knowledge": "Generated content" } from local_deep_research.report_generator import ( IntegratedReportGenerator, ) generator = IntegratedReportGenerator(llm=mock_llm) generator.search_system = mock_search_system yield generator def test_section_with_empty_subsections_creates_default( self, report_generator ): """Section with empty subsections list should get a default subsection.""" structure = [{"name": "Standalone Section", "subsections": []}] initial_findings = { "current_knowledge": "test", "questions_by_iteration": {}, } sections = report_generator._research_and_generate_sections( initial_findings, structure, "test query" ) assert "Standalone Section" in sections # analyze_topic should have been called for the auto-created subsection report_generator.search_system.analyze_topic.assert_called() def test_section_with_pipe_in_name_parsed_for_subsection( self, report_generator ): """Section name with pipe should be parsed into subsection name and purpose.""" structure = [ {"name": "Main Topic | Purpose of this section", "subsections": []} ] initial_findings = { "current_knowledge": "test", "questions_by_iteration": {}, } sections = report_generator._research_and_generate_sections( initial_findings, structure, "test query" ) # The section should be processed with subsections created from the pipe-split name assert "Main Topic | Purpose of this section" in sections def test_multiple_empty_sections_each_get_default(self, report_generator): """Multiple sections without subsections each get their own default.""" structure = [ {"name": "Section A", "subsections": []}, {"name": "Section B", "subsections": []}, {"name": "Section C", "subsections": []}, ] initial_findings = { "current_knowledge": "test", "questions_by_iteration": {}, } sections = report_generator._research_and_generate_sections( initial_findings, structure, "test query" ) assert len(sections) == 3 assert "Section A" in sections assert "Section B" in sections assert "Section C" in sections class TestMaxIterationsRestoration: """Tests for max_iterations preservation and restoration.""" @pytest.fixture def report_generator(self): """Create a report generator with mocked dependencies.""" with patch("local_deep_research.report_generator.AdvancedSearchSystem"): with patch( "local_deep_research.report_generator.get_llm" ) as mock_get_llm: mock_llm = MagicMock() mock_get_llm.return_value = mock_llm mock_search_system = MagicMock() mock_search_system.all_links_of_system = [] mock_search_system.max_iterations = 5 # Original value mock_search_system.analyze_topic.return_value = { "current_knowledge": "Content" } # Create a mock strategy with real settings_snapshot dict mock_strategy = MagicMock() mock_strategy.settings_snapshot = {"search.iterations": 5} mock_strategy.max_iterations = 5 mock_search_system.strategy = mock_strategy from local_deep_research.report_generator import ( IntegratedReportGenerator, ) generator = IntegratedReportGenerator(llm=mock_llm) generator.search_system = mock_search_system yield generator def test_max_iterations_restored_after_section(self, report_generator): """max_iterations should be restored to original value after each subsection.""" original_max = report_generator.search_system.max_iterations structure = [ { "name": "Test", "subsections": [{"name": "Sub", "purpose": "Purpose"}], } ] report_generator._research_and_generate_sections( {"current_knowledge": "test", "questions_by_iteration": {}}, structure, "query", ) # After generation, max_iterations should be back to original assert report_generator.search_system.max_iterations == original_max def test_max_iterations_set_to_one_during_subsection_research( self, report_generator ): """search.iterations should be set to 1 in strategy settings_snapshot during subsection research.""" iterations_during_search = [] def capture_iterations(*args, **kwargs): iterations_during_search.append( report_generator.search_system.strategy.settings_snapshot.get( "search.iterations" ) ) return {"current_knowledge": "Content"} report_generator.search_system.analyze_topic.side_effect = ( capture_iterations ) structure = [ { "name": "Test", "subsections": [ {"name": "Sub1", "purpose": "P1"}, {"name": "Sub2", "purpose": "P2"}, ], } ] report_generator._research_and_generate_sections( {"current_knowledge": "test", "questions_by_iteration": {}}, structure, "query", ) # Each subsection should have had search.iterations=1 assert all(i == 1 for i in iterations_during_search) def test_max_iterations_restored_after_multiple_sections( self, report_generator ): """max_iterations restoration should work across multiple sections.""" original_max = report_generator.search_system.max_iterations structure = [ { "name": "Section1", "subsections": [{"name": "Sub1", "purpose": "P1"}], }, { "name": "Section2", "subsections": [ {"name": "Sub2a", "purpose": "P2a"}, {"name": "Sub2b", "purpose": "P2b"}, ], }, ] report_generator._research_and_generate_sections( {"current_knowledge": "test", "questions_by_iteration": {}}, structure, "query", ) assert report_generator.search_system.max_iterations == original_max class TestStateIsolationBetweenSections: """Tests for ensuring state doesn't leak between sections.""" @pytest.fixture def report_generator(self): """Create a report generator with mocked dependencies.""" with patch("local_deep_research.report_generator.AdvancedSearchSystem"): with patch( "local_deep_research.report_generator.get_llm" ) as mock_get_llm: mock_llm = MagicMock() mock_get_llm.return_value = mock_llm mock_search_system = MagicMock() mock_search_system.all_links_of_system = [] mock_search_system.max_iterations = 3 mock_search_system.analyze_topic.return_value = { "current_knowledge": "Content" } from local_deep_research.report_generator import ( IntegratedReportGenerator, ) generator = IntegratedReportGenerator(llm=mock_llm) generator.search_system = mock_search_system yield generator def test_sections_content_independent(self, report_generator): """Each section should receive independent content.""" call_count = [0] def unique_content(*args, **kwargs): call_count[0] += 1 return {"current_knowledge": f"Content for call {call_count[0]}"} report_generator.search_system.analyze_topic.side_effect = ( unique_content ) structure = [ { "name": "Section1", "subsections": [{"name": "Sub1", "purpose": "P1"}], }, { "name": "Section2", "subsections": [{"name": "Sub2", "purpose": "P2"}], }, ] sections = report_generator._research_and_generate_sections( {"current_knowledge": "test", "questions_by_iteration": {}}, structure, "query", ) # Each section should have different content assert "Content for call 1" in sections["Section1"] assert "Content for call 2" in sections["Section2"] def test_context_includes_other_sections(self, report_generator): """Each subsection query should include context about other sections.""" captured_queries = [] def capture_query(query, *args, **kwargs): captured_queries.append(query) return {"current_knowledge": "Content"} report_generator.search_system.analyze_topic.side_effect = capture_query structure = [ { "name": "Introduction", "subsections": [{"name": "Overview", "purpose": "Intro"}], }, { "name": "Main Content", "subsections": [{"name": "Details", "purpose": "Main info"}], }, { "name": "Conclusion", "subsections": [{"name": "Summary", "purpose": "Wrap up"}], }, ] report_generator._research_and_generate_sections( {"current_knowledge": "test", "questions_by_iteration": {}}, structure, "test query", ) # First section query should mention other sections assert ( "Main Content" in captured_queries[0] or "Conclusion" in captured_queries[0] ) # Middle section should mention Introduction and Conclusion assert ( "Introduction" in captured_queries[1] or "Conclusion" in captured_queries[1] ) class TestEmptyResultHandling: """Tests for handling empty or missing content from search system.""" @pytest.fixture def report_generator(self): """Create a report generator with mocked dependencies.""" with patch("local_deep_research.report_generator.AdvancedSearchSystem"): with patch( "local_deep_research.report_generator.get_llm" ) as mock_get_llm: mock_llm = MagicMock() mock_get_llm.return_value = mock_llm mock_search_system = MagicMock() mock_search_system.all_links_of_system = [] mock_search_system.max_iterations = 3 from local_deep_research.report_generator import ( IntegratedReportGenerator, ) generator = IntegratedReportGenerator(llm=mock_llm) generator.search_system = mock_search_system yield generator def test_empty_current_knowledge_shows_placeholder(self, report_generator): """Empty current_knowledge should result in placeholder text.""" report_generator.search_system.analyze_topic.return_value = { "current_knowledge": "" } structure = [ { "name": "Test", "subsections": [{"name": "Sub", "purpose": "Purpose"}], } ] sections = report_generator._research_and_generate_sections( {"current_knowledge": "test", "questions_by_iteration": {}}, structure, "query", ) assert "Limited information was found" in sections["Test"] def test_none_current_knowledge_shows_placeholder(self, report_generator): """None current_knowledge should result in placeholder text.""" report_generator.search_system.analyze_topic.return_value = { "current_knowledge": None } structure = [ { "name": "Test", "subsections": [{"name": "Sub", "purpose": "Purpose"}], } ] sections = report_generator._research_and_generate_sections( {"current_knowledge": "test", "questions_by_iteration": {}}, structure, "query", ) assert "Limited information was found" in sections["Test"] def test_missing_current_knowledge_key_shows_placeholder( self, report_generator ): """Missing current_knowledge key should result in placeholder text.""" report_generator.search_system.analyze_topic.return_value = {} structure = [ { "name": "Test", "subsections": [{"name": "Sub", "purpose": "Purpose"}], } ] sections = report_generator._research_and_generate_sections( {"current_knowledge": "test", "questions_by_iteration": {}}, structure, "query", ) assert "Limited information was found" in sections["Test"]