"""Edge-case tests for report_generator — structure parsing boundaries.""" from unittest.mock import Mock import pytest from local_deep_research.report_generator import IntegratedReportGenerator @pytest.fixture def mock_search_system(): """Create a mock search system.""" mock = Mock() mock.all_links_of_system = [] return mock @pytest.fixture def generator(mock_llm, mock_search_system, monkeypatch): """Create a report generator with mocked dependencies.""" monkeypatch.setattr( "local_deep_research.report_generator.get_llm", lambda: mock_llm ) return IntegratedReportGenerator( search_system=mock_search_system, llm=mock_llm ) class TestDetermineReportStructureNoDelimiters: """Verify LLM response without STRUCTURE/END_STRUCTURE returns empty structure.""" def test_no_structure_delimiters(self, generator): """Response without STRUCTURE markers → empty structure list.""" generator.model.invoke.return_value = Mock( content="Here is my analysis of the topic. It covers many areas." ) findings = {"current_knowledge": "Some knowledge about the topic."} structure = generator._determine_report_structure( findings, "test query" ) # No numbered section lines → empty structure assert structure == [] class TestDetermineReportStructureSectionWithoutPeriod: """A numbered section line without a period is skipped gracefully (no IndexError).""" def test_section_without_period_is_skipped(self, generator): """'1 Section Name' (no period) is skipped rather than raising IndexError.""" generator.model.invoke.return_value = Mock( content=("STRUCTURE\n1 Section Without Period\nEND_STRUCTURE") ) findings = {"current_knowledge": "Some knowledge."} # Previously line.split(".")[1] raised IndexError; now the line is skipped. structure = generator._determine_report_structure( findings, "test query" ) assert structure == [] def test_valid_and_dotless_lines_mixed(self, generator): """A valid numbered line still parses even when a dotless one precedes it.""" generator.model.invoke.return_value = Mock( content=( "STRUCTURE\n1 No Period Here\n2. Real Section\nEND_STRUCTURE" ) ) findings = {"current_knowledge": "Some knowledge."} structure = generator._determine_report_structure( findings, "test query" ) assert len(structure) == 1 assert structure[0]["name"] == "Real Section" class TestDetermineReportStructureSubsectionBeforeAnySection: """Verify subsection line before any numbered section doesn't crash.""" def test_subsection_before_any_section(self, generator): """'- Subsection' before any numbered section → current_section is None.""" generator.model.invoke.return_value = Mock( content=( "STRUCTURE\n" "- Orphan Subsection | some purpose\n" "1. Actual Section\n" " - Valid Subsection | valid purpose\n" "END_STRUCTURE" ) ) findings = {"current_knowledge": "Some knowledge."} structure = generator._determine_report_structure( findings, "test query" ) # The orphan subsection should be safely skipped (current_section guard) # Only the actual section with its valid subsection should be parsed assert len(structure) == 1 assert structure[0]["name"] == "Actual Section" assert len(structure[0]["subsections"]) == 1 assert structure[0]["subsections"][0]["name"] == "Valid Subsection" class TestDetermineReportStructureRemovesSourcesSection: """Verify last section named 'References' or 'Sources' is auto-removed.""" def test_removes_references_section(self, generator): """Last section named 'References' should be auto-removed.""" generator.model.invoke.return_value = Mock( content=( "STRUCTURE\n" "1. Introduction\n" " - Overview | provide overview\n" "2. Key Findings\n" " - Results | present results\n" "3. References\n" " - Bibliography | list sources\n" "END_STRUCTURE" ) ) findings = {"current_knowledge": "Some knowledge."} structure = generator._determine_report_structure( findings, "test query" ) # "References" matches source_keywords → removed assert len(structure) == 2 assert structure[0]["name"] == "Introduction" assert structure[1]["name"] == "Key Findings" # "References" section should be gone section_names = [s["name"] for s in structure] assert "References" not in section_names def test_does_not_remove_non_last_source_section(self, generator): """Source-related section that is NOT last should be kept.""" generator.model.invoke.return_value = Mock( content=( "STRUCTURE\n" "1. Sources Overview\n" " - Data Sources | describe data origins\n" "2. Analysis\n" " - Methods | explain methods\n" "END_STRUCTURE" ) ) findings = {"current_knowledge": "Some knowledge."} structure = generator._determine_report_structure( findings, "test query" ) # "Sources Overview" is first, not last → should NOT be removed # "Analysis" is last and doesn't match source keywords → kept assert len(structure) == 2 assert structure[0]["name"] == "Sources Overview" assert structure[1]["name"] == "Analysis"