"""Comprehensive tests for report_generator.py targeting uncovered paths. Focuses on areas with gaps in existing test suites: - close() resource cleanup with _owns_llm flag - _format_final_report TOC assembly, section ordering, missing sections, metadata - _truncate_at_sentence_boundary additional edge cases - _build_previous_context boundary conditions - _research_and_generate_sections iteration restore on exception, pipe in section name - _generate_error_report format - generate_report progress callback sequencing """ from unittest.mock import MagicMock, patch import pytest from local_deep_research.report_generator import ( IntegratedReportGenerator, DEFAULT_MAX_CONTEXT_CHARS, DEFAULT_MAX_CONTEXT_SECTIONS, ) MODULE = "local_deep_research.report_generator" # --------------------------------------------------------------------------- # Shared fixtures # --------------------------------------------------------------------------- @pytest.fixture def mock_search_system(): """Minimal mock search system with strategy.""" system = MagicMock() system.strategy = MagicMock() system.strategy.settings_snapshot = {"search.iterations": 3} system.strategy.max_iterations = 3 system.all_links_of_system = [] system.analyze_topic.return_value = {"current_knowledge": "mock content"} return system @pytest.fixture def mock_model(): """Minimal mock LLM.""" model = MagicMock() model.invoke.return_value = MagicMock(content="STRUCTURE\nEND_STRUCTURE") return model @pytest.fixture def generator(mock_search_system, mock_model): """IntegratedReportGenerator with all deps mocked via __new__.""" gen = IntegratedReportGenerator.__new__(IntegratedReportGenerator) gen.search_system = mock_search_system gen.model = mock_model gen.searches_per_section = 2 gen.max_context_sections = DEFAULT_MAX_CONTEXT_SECTIONS gen.max_context_chars = DEFAULT_MAX_CONTEXT_CHARS gen._owns_llm = False return gen # --------------------------------------------------------------------------- # close() -- resource cleanup # --------------------------------------------------------------------------- class TestClose: """Tests for close() method and _owns_llm lifecycle.""" def test_close_when_owns_llm_calls_close(self, generator, mock_model): """When _owns_llm is True, close() calls model.close().""" generator._owns_llm = True generator.model = mock_model generator.close() mock_model.close.assert_called_once() def test_close_when_not_owns_llm_does_nothing(self, generator, mock_model): """When _owns_llm is False, close() must not call model.close().""" generator._owns_llm = False generator.model = mock_model generator.close() mock_model.close.assert_not_called() def test_close_when_model_is_none(self, generator): """When model is None (regardless of _owns_llm), close() is safe.""" generator._owns_llm = True generator.model = None # Should not raise generator.close() def test_owns_llm_set_true_when_no_search_system_or_llm(self): """Constructor sets _owns_llm=True when neither search_system nor llm given.""" with ( patch(f"{MODULE}.get_llm") as mock_get_llm, patch(f"{MODULE}.AdvancedSearchSystem"), patch( f"{MODULE}.get_setting_from_snapshot", side_effect=lambda k, default=None, settings_snapshot=None: ( default ), ), ): mock_get_llm.return_value = MagicMock() gen = IntegratedReportGenerator() assert gen._owns_llm is True def test_owns_llm_set_false_when_search_system_provided( self, mock_search_system ): """Constructor sets _owns_llm=False when search_system is provided.""" mock_search_system.model = MagicMock() with patch( f"{MODULE}.get_setting_from_snapshot", side_effect=lambda k, default=None, settings_snapshot=None: default, ): gen = IntegratedReportGenerator(search_system=mock_search_system) assert gen._owns_llm is False def test_owns_llm_set_false_when_llm_provided(self): """Constructor sets _owns_llm=False when llm is provided.""" with ( patch(f"{MODULE}.AdvancedSearchSystem"), patch( f"{MODULE}.get_setting_from_snapshot", side_effect=lambda k, default=None, settings_snapshot=None: ( default ), ), ): gen = IntegratedReportGenerator(llm=MagicMock()) assert gen._owns_llm is False # --------------------------------------------------------------------------- # _format_final_report -- detailed TOC and assembly # --------------------------------------------------------------------------- class TestFormatFinalReportDetailed: """Detailed tests for _format_final_report output structure.""" @pytest.fixture def _patch_importlib(self): """Patch importlib.import_module to return a mock with format_links_to_markdown.""" with patch(f"{MODULE}.importlib") as mock_imp: mock_utils = MagicMock() mock_utils.search_utilities.format_links_to_markdown.return_value = "- [Link1](http://a.com)" mock_imp.import_module.return_value = mock_utils yield mock_imp def test_toc_numbering_matches_structure_order( self, generator, _patch_importlib ): """TOC entries use 1-based numbering matching structure list order.""" structure = [ {"name": "Alpha", "subsections": [{"name": "A1", "purpose": "p1"}]}, {"name": "Beta", "subsections": [{"name": "B1", "purpose": "p2"}]}, {"name": "Gamma", "subsections": [{"name": "G1", "purpose": "p3"}]}, ] sections = { "Alpha": "Alpha content", "Beta": "Beta content", "Gamma": "Gamma content", } report = generator._format_final_report(sections, structure, "q") content = report["content"] assert "1. **Alpha**" in content assert "2. **Beta**" in content assert "3. **Gamma**" in content def test_toc_subsection_numbering(self, generator, _patch_importlib): """Subsections use i.j numbering format.""" structure = [ { "name": "Section", "subsections": [ {"name": "Sub1", "purpose": "first"}, {"name": "Sub2", "purpose": "second"}, {"name": "Sub3", "purpose": "third"}, ], } ] sections = {"Section": "content"} report = generator._format_final_report(sections, structure, "q") content = report["content"] assert "1.1 Sub1" in content assert "1.2 Sub2" in content assert "1.3 Sub3" in content def test_toc_subsection_includes_purpose_in_italics( self, generator, _patch_importlib ): """Subsection TOC entries include purpose in italics after pipe.""" structure = [ { "name": "S", "subsections": [ {"name": "Detail", "purpose": "explain details"}, {"name": "Extra", "purpose": "additional info"}, ], } ] sections = {"S": "content"} report = generator._format_final_report(sections, structure, "q") content = report["content"] assert "_explain details_" in content def test_section_content_appears_in_structure_order( self, generator, _patch_importlib ): """Section content is emitted in the order defined by structure, not dict order.""" structure = [ {"name": "Zebra", "subsections": []}, {"name": "Apple", "subsections": []}, ] sections = { "Apple": "Apple body text", "Zebra": "Zebra body text", } report = generator._format_final_report(sections, structure, "q") content = report["content"] zebra_pos = content.index("Zebra body text") apple_pos = content.index("Apple body text") assert zebra_pos < apple_pos, "Sections must appear in structure order" def test_missing_section_content_omitted_gracefully( self, generator, _patch_importlib ): """If a section name is in structure but not in sections dict, it is skipped.""" structure = [ {"name": "Present", "subsections": []}, {"name": "Missing", "subsections": []}, ] sections = {"Present": "Present content"} report = generator._format_final_report(sections, structure, "q") content = report["content"] assert "Present content" in content def test_sources_section_appended_at_end(self, generator, _patch_importlib): """Final report ends with ## Sources followed by formatted links.""" structure = [{"name": "A", "subsections": []}] sections = {"A": "body"} report = generator._format_final_report(sections, structure, "q") content = report["content"] assert content.rstrip().endswith("- [Link1](http://a.com)") def test_research_summary_present(self, generator, _patch_importlib): """Report contains '# Research Summary' section.""" structure = [{"name": "A", "subsections": []}] sections = {"A": "body"} report = generator._format_final_report(sections, structure, "q") assert "# Research Summary" in report["content"] def test_metadata_searches_per_section(self, generator, _patch_importlib): """Metadata includes searches_per_section value.""" generator.searches_per_section = 7 structure = [{"name": "A", "subsections": []}] sections = {"A": "body"} report = generator._format_final_report(sections, structure, "q") assert report["metadata"]["searches_per_section"] == 7 def test_metadata_initial_sources_counts_links( self, generator, _patch_importlib ): """Metadata initial_sources equals len(all_links_of_system).""" generator.search_system.all_links_of_system = [ {"link": "a"}, {"link": "b"}, {"link": "c"}, ] structure = [] sections = {} report = generator._format_final_report(sections, structure, "q") assert report["metadata"]["initial_sources"] == 3 def test_metadata_generated_at_is_iso_format( self, generator, _patch_importlib ): """generated_at is a valid ISO 8601 string.""" from datetime import datetime structure = [] sections = {} report = generator._format_final_report(sections, structure, "q") # Should not raise datetime.fromisoformat(report["metadata"]["generated_at"]) def test_empty_structure_produces_valid_report( self, generator, _patch_importlib ): """Empty structure list still produces a report with TOC header and sources.""" report = generator._format_final_report({}, [], "q") assert "Table of Contents" in report["content"] assert "## Sources" in report["content"] assert report["metadata"]["sections_researched"] == 0 # --------------------------------------------------------------------------- # _truncate_at_sentence_boundary -- additional edge cases # --------------------------------------------------------------------------- class TestTruncateEdgeCases: """Additional edge cases for _truncate_at_sentence_boundary.""" def test_text_with_only_punctuation(self, generator): """Text like '...' should not crash.""" result = generator._truncate_at_sentence_boundary("...", 2) assert result == "..\n[...truncated]" def test_sentence_ending_at_exactly_max_chars(self, generator): """When sentence ends exactly at max_chars, use that boundary.""" text = "Hello. World." # max_chars=6: truncated = "Hello." -> period at index 5 followed by end result = generator._truncate_at_sentence_boundary(text, 6) assert result.startswith("Hello.") assert "[...truncated]" in result def test_max_chars_one(self, generator): """max_chars=1 with longer text.""" result = generator._truncate_at_sentence_boundary("ab", 1) assert result == "a\n[...truncated]" def test_exclamation_at_end_of_truncated_region(self, generator): """Exclamation mark at very end of truncated region is a valid boundary.""" text = "Stop! More text follows here and keeps going." # "Stop!" is 5 chars, max_chars=5: truncated = "Stop!" result = generator._truncate_at_sentence_boundary(text, 5) assert "Stop!" in result assert "[...truncated]" in result def test_mixed_sentence_endings(self, generator): """Text with mixed .!? picks last valid one before limit.""" text = "First. Second! Third? Fourth sentence that is very very long." # max_chars=25: truncated = "First. Second! Third? Fou" result = generator._truncate_at_sentence_boundary(text, 25) # Last valid boundary: "?" at index 20, followed by " ", 21 > 20 (80% of 25) assert "Third?" in result assert "[...truncated]" in result def test_newline_after_period_is_valid_boundary(self, generator): """Period followed by newline counts as sentence boundary.""" text = ( "Done.\nMore text that extends beyond the character limit by a lot." ) result = generator._truncate_at_sentence_boundary(text, 10) assert "Done." in result assert "[...truncated]" in result def test_period_in_middle_of_word_not_boundary(self, generator): """Period not followed by space/newline/end is not a boundary.""" # "file.txt" has a period but it's followed by 't' text = "file.txt is the name of the file and it is important to know." result = generator._truncate_at_sentence_boundary(text, 12) # "file.txt is " -> period at 4 followed by 't', not space # No valid boundary -> hard truncation assert result == text[:12] + "\n[...truncated]" def test_unicode_text_truncation(self, generator): """Unicode characters are handled correctly in truncation.""" text = "Bonjour le monde. C'est magnifique! Plus de texte ici." result = generator._truncate_at_sentence_boundary(text, 40) assert "[...truncated]" in result # --------------------------------------------------------------------------- # _build_previous_context -- boundary conditions # --------------------------------------------------------------------------- class TestBuildPreviousContextBoundary: """Boundary conditions for _build_previous_context.""" def test_exactly_max_context_sections_findings(self, generator): """When findings count equals max_context_sections, all are included.""" generator.max_context_sections = 3 findings = ["Finding A", "Finding B", "Finding C"] result = generator._build_previous_context(findings) assert "Finding A" in result assert "Finding B" in result assert "Finding C" in result def test_one_more_than_max_drops_oldest(self, generator): """When findings count is max+1, oldest is dropped.""" generator.max_context_sections = 2 findings = ["Old", "Mid", "New"] result = generator._build_previous_context(findings) assert "Old" not in result assert "Mid" in result assert "New" in result def test_context_chars_exactly_at_limit_no_truncation(self, generator): """When context length equals max_context_chars, no truncation marker.""" generator.max_context_chars = 20 # Single finding exactly 20 chars finding = "x" * 20 result = generator._build_previous_context([finding]) assert "[...truncated]" not in result def test_context_chars_one_over_limit_triggers_truncation(self, generator): """When context length is max_context_chars + 1, truncation happens.""" generator.max_context_chars = 20 finding = "x" * 21 result = generator._build_previous_context([finding]) assert "[...truncated]" in result def test_max_context_sections_zero_returns_all(self, generator): """max_context_sections=0 -> [-0:] is full list in Python, so all are included.""" generator.max_context_sections = 0 result = generator._build_previous_context(["A", "B"]) # [-0:] == [0:] == full list, so both findings appear assert "A" in result assert "B" in result def test_single_finding_with_large_char_limit(self, generator): """Single small finding with huge char limit is not truncated.""" generator.max_context_chars = 100000 result = generator._build_previous_context(["Small finding."]) assert "Small finding." in result assert "[...truncated]" not in result # --------------------------------------------------------------------------- # _research_and_generate_sections -- iteration override and restore # --------------------------------------------------------------------------- class TestIterationOverrideRestore: """Tests that iteration settings are restored even on exception.""" def test_settings_restored_after_analyze_topic_exception( self, generator, mock_search_system ): """If analyze_topic raises, iteration settings are still restored.""" mock_search_system.strategy.settings_snapshot = {"search.iterations": 5} mock_search_system.strategy.max_iterations = 5 mock_search_system.analyze_topic.side_effect = RuntimeError("boom") structure = [ {"name": "S", "subsections": [{"name": "Sub", "purpose": "p"}]} ] with pytest.raises(RuntimeError, match="boom"): generator._research_and_generate_sections( {"current_knowledge": "init"}, structure, "q" ) # Settings must be restored assert ( mock_search_system.strategy.settings_snapshot["search.iterations"] == 5 ) assert mock_search_system.strategy.max_iterations == 5 def test_settings_restored_when_key_not_originally_present( self, generator, mock_search_system ): """If search.iterations was not in snapshot originally, it is removed after.""" mock_search_system.strategy.settings_snapshot = {} # no key mock_search_system.strategy.max_iterations = 3 mock_search_system.analyze_topic.return_value = { "current_knowledge": "ok" } structure = [ {"name": "S", "subsections": [{"name": "Sub", "purpose": "p"}]} ] generator._research_and_generate_sections( {"current_knowledge": "init"}, structure, "q" ) # Key should be removed since it was not originally present assert ( "search.iterations" not in mock_search_system.strategy.settings_snapshot ) def test_iteration_set_to_one_during_analyze_topic( self, generator, mock_search_system ): """During analyze_topic, search.iterations should be 1.""" captured_value = {} def capture_settings(query): captured_value["iterations"] = ( mock_search_system.strategy.settings_snapshot.get( "search.iterations" ) ) captured_value["max_iterations"] = ( mock_search_system.strategy.max_iterations ) return {"current_knowledge": "content"} mock_search_system.analyze_topic.side_effect = capture_settings mock_search_system.strategy.settings_snapshot = {"search.iterations": 5} mock_search_system.strategy.max_iterations = 5 structure = [ {"name": "S", "subsections": [{"name": "Sub", "purpose": "p"}]} ] generator._research_and_generate_sections( {"current_knowledge": "init"}, structure, "q" ) assert captured_value["iterations"] == 1 assert captured_value["max_iterations"] == 1 # --------------------------------------------------------------------------- # _research_and_generate_sections -- section with pipe in name # --------------------------------------------------------------------------- class TestSectionWithPipeInName: """When a section has no subsections and contains a pipe, it splits into name|purpose.""" def test_pipe_in_section_name_creates_subsection_with_purpose( self, generator, mock_search_system ): """Section name 'Overview | General introduction' becomes subsection.""" mock_search_system.analyze_topic.return_value = { "current_knowledge": "body" } structure = [ {"name": "Overview | General introduction", "subsections": []} ] sections = generator._research_and_generate_sections( {"current_knowledge": "init"}, structure, "q" ) assert "Overview | General introduction" in sections mock_search_system.analyze_topic.assert_called_once() def test_no_pipe_in_section_name_uses_section_name_as_subsection( self, generator, mock_search_system ): """Section without pipe and no subsections uses section name as subsection name.""" mock_search_system.analyze_topic.return_value = { "current_knowledge": "body" } structure = [{"name": "Introduction", "subsections": []}] sections = generator._research_and_generate_sections( {"current_knowledge": "init"}, structure, "q" ) assert "Introduction" in sections # The query should contain the section name query_arg = mock_search_system.analyze_topic.call_args[0][0] assert "Introduction" in query_arg # --------------------------------------------------------------------------- # _research_and_generate_sections -- empty current_knowledge handling # --------------------------------------------------------------------------- class TestEmptySubsectionResults: """When analyze_topic returns empty/None current_knowledge.""" def test_none_current_knowledge_shows_limited_info_message( self, generator, mock_search_system ): """None current_knowledge produces 'Limited information' placeholder.""" mock_search_system.analyze_topic.return_value = { "current_knowledge": None } structure = [ {"name": "S", "subsections": [{"name": "Sub", "purpose": "p"}]} ] sections = generator._research_and_generate_sections( {"current_knowledge": "init"}, structure, "q" ) assert "Limited information" in sections["S"] def test_empty_string_current_knowledge_shows_limited_info( self, generator, mock_search_system ): """Empty string current_knowledge produces 'Limited information' placeholder.""" mock_search_system.analyze_topic.return_value = { "current_knowledge": "" } structure = [ {"name": "S", "subsections": [{"name": "Sub", "purpose": "p"}]} ] sections = generator._research_and_generate_sections( {"current_knowledge": "init"}, structure, "q" ) assert "Limited information" in sections["S"] def test_missing_current_knowledge_key_shows_limited_info( self, generator, mock_search_system ): """Missing current_knowledge key produces 'Limited information' placeholder.""" mock_search_system.analyze_topic.return_value = {} structure = [ {"name": "S", "subsections": [{"name": "Sub", "purpose": "p"}]} ] sections = generator._research_and_generate_sections( {"current_knowledge": "init"}, structure, "q" ) assert "Limited information" in sections["S"] # --------------------------------------------------------------------------- # _research_and_generate_sections -- subsection header logic # --------------------------------------------------------------------------- class TestSubsectionHeaders: """Tests for subsection header inclusion based on subsection count.""" def test_single_subsection_no_header(self, generator, mock_search_system): """Section with one subsection does not add ## subsection header.""" mock_search_system.analyze_topic.return_value = { "current_knowledge": "body" } structure = [ {"name": "S", "subsections": [{"name": "OnlySub", "purpose": "p"}]} ] sections = generator._research_and_generate_sections( {"current_knowledge": "init"}, structure, "q" ) assert "## OnlySub" not in sections["S"] def test_multiple_subsections_add_headers( self, generator, mock_search_system ): """Section with multiple subsections adds ## headers for each.""" mock_search_system.analyze_topic.return_value = { "current_knowledge": "body" } structure = [ { "name": "S", "subsections": [ {"name": "SubA", "purpose": "pA"}, {"name": "SubB", "purpose": "pB"}, ], } ] sections = generator._research_and_generate_sections( {"current_knowledge": "init"}, structure, "q" ) assert "## 1.1 SubA" in sections["S"] assert "## 1.2 SubB" in sections["S"] # --------------------------------------------------------------------------- # _generate_error_report -- output format # --------------------------------------------------------------------------- class TestGenerateErrorReport: """Tests for _generate_error_report output.""" def test_contains_error_header(self, generator): result = generator._generate_error_report("my query", "oops") assert "=== ERROR REPORT ===" in result def test_contains_query(self, generator): result = generator._generate_error_report("my query", "oops") assert "my query" in result def test_contains_error_message(self, generator): result = generator._generate_error_report("q", "something broke") assert "something broke" in result def test_empty_error_message(self, generator): result = generator._generate_error_report("q", "") assert "Error:" in result # --------------------------------------------------------------------------- # generate_report -- progress callback ordering # --------------------------------------------------------------------------- class TestGenerateReportProgressOrdering: """Verify progress callback phase order in generate_report.""" def test_phases_in_correct_order( self, generator, mock_search_system, mock_model ): """Phases: structure -> section_research -> formatting -> complete.""" mock_model.invoke.return_value = MagicMock( content="STRUCTURE\n1. Intro\n - Over | purpose\nEND_STRUCTURE" ) mock_search_system.analyze_topic.return_value = { "current_knowledge": "text" } with patch(f"{MODULE}.importlib") as mock_imp: mock_utils = MagicMock() mock_utils.search_utilities.format_links_to_markdown.return_value = "" mock_imp.import_module.return_value = mock_utils callback = MagicMock() generator.generate_report( {"current_knowledge": "findings"}, "q", progress_callback=callback, ) phases = [c.args[2]["phase"] for c in callback.call_args_list] # First must be structure, last must be complete assert phases[0] == "report_structure" assert phases[-1] == "report_complete" # formatting must come after all section research fmt_idx = phases.index("report_formatting") research_indices = [ i for i, p in enumerate(phases) if p == "report_section_research" ] if research_indices: assert fmt_idx > max(research_indices) def test_no_callback_does_not_crash( self, generator, mock_search_system, mock_model ): """generate_report works without progress_callback.""" mock_model.invoke.return_value = MagicMock( content="STRUCTURE\n1. S\n - Sub | p\nEND_STRUCTURE" ) mock_search_system.analyze_topic.return_value = { "current_knowledge": "t" } with patch(f"{MODULE}.importlib") as mock_imp: mock_utils = MagicMock() mock_utils.search_utilities.format_links_to_markdown.return_value = "" mock_imp.import_module.return_value = mock_utils result = generator.generate_report({"current_knowledge": "f"}, "q") assert "content" in result # --------------------------------------------------------------------------- # _generate_sections (deprecated) -- returns empty dict # --------------------------------------------------------------------------- class TestDeprecatedGenerateSections: """The deprecated _generate_sections always returns {}.""" def test_returns_empty_dict(self, generator): result = generator._generate_sections({}, {}, [], "q") assert result == {} def test_ignores_all_arguments(self, generator): result = generator._generate_sections( {"current_knowledge": "data"}, {"sec": [{"content": "c"}]}, [{"name": "S", "subsections": []}], "query", ) assert result == {} # --------------------------------------------------------------------------- # _determine_report_structure -- think tag removal # --------------------------------------------------------------------------- class TestDetermineReportStructureThinkTags: """Verify blocks are stripped from the LLM response before parsing. The response is normalized via ``get_llm_response_text`` (which strips think tags), so reasoning content cannot leak into the parsed structure. """ def test_think_tags_stripped_before_parsing(self, generator, mock_model): """Numbered lines inside must not become bogus sections.""" mock_model.invoke.return_value = MagicMock( content=( "\n" "1. Bogus reasoning section\n" "\n" "STRUCTURE\n1. Real\nEND_STRUCTURE" ) ) findings = {"current_knowledge": "knowledge"} structure = generator._determine_report_structure(findings, "q") # Without stripping, "1. Bogus reasoning section" would parse as a # section; with stripping only the real section remains. assert len(structure) == 1 assert structure[0]["name"] == "Real" def test_string_response_does_not_crash(self, generator, mock_model): """A raw string model return (no .content) is handled gracefully.""" mock_model.invoke.return_value = "STRUCTURE\n1. Real\nEND_STRUCTURE" findings = {"current_knowledge": "knowledge"} structure = generator._determine_report_structure(findings, "q") assert len(structure) == 1 assert structure[0]["name"] == "Real" # --------------------------------------------------------------------------- # _research_and_generate_sections -- questions_by_iteration propagation # --------------------------------------------------------------------------- class TestQuestionsPropagation: """Tests for questions_by_iteration being set on search system and strategy.""" def test_questions_set_on_strategy_when_present( self, generator, mock_search_system ): """questions_by_iteration from initial_findings is propagated to strategy.""" mock_search_system.questions_by_iteration = {} mock_search_system.strategy.questions_by_iteration = {} initial = { "current_knowledge": "init", "questions_by_iteration": {"0": ["Q1"], "1": ["Q2", "Q3"]}, } structure = [ {"name": "S", "subsections": [{"name": "Sub", "purpose": "p"}]} ] generator._research_and_generate_sections(initial, structure, "q") assert mock_search_system.strategy.questions_by_iteration == { "0": ["Q1"], "1": ["Q2", "Q3"], } def test_no_questions_key_does_not_crash( self, generator, mock_search_system ): """Missing questions_by_iteration in initial_findings is fine.""" initial = {"current_knowledge": "init"} structure = [ {"name": "S", "subsections": [{"name": "Sub", "purpose": "p"}]} ] # Should not raise generator._research_and_generate_sections(initial, structure, "q") def test_empty_questions_dict_does_not_propagate( self, generator, mock_search_system ): """Empty questions_by_iteration dict is falsy, so nothing is set.""" original_q = {"original": True} mock_search_system.strategy.questions_by_iteration = original_q initial = {"current_knowledge": "init", "questions_by_iteration": {}} structure = [ {"name": "S", "subsections": [{"name": "Sub", "purpose": "p"}]} ] generator._research_and_generate_sections(initial, structure, "q") # Empty dict is falsy -> original should remain assert mock_search_system.strategy.questions_by_iteration is original_q