Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

745 lines
27 KiB
Python

"""
Tests for report_generator.py - Structure Parsing Edge Cases
Tests cover the parsing of LLM-generated report structures, including:
- Subsection parsing with pipes
- Malformed structure handling
- Source section filtering
These tests address real bugs like the one fixed in commit 5128c1d6 for pipes in purpose.
"""
from unittest.mock import MagicMock, patch
import pytest
class TestSubsectionParsingEdgeCases:
"""Tests for edge cases in subsection parsing."""
@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
from local_deep_research.report_generator import (
IntegratedReportGenerator,
)
generator = IntegratedReportGenerator(llm=mock_llm)
yield generator
def test_multiple_pipes_in_purpose(self, report_generator):
"""'Overview | What is x | How it works' preserves all after first pipe."""
# Simulate LLM response with multiple pipes
response = """
STRUCTURE
1. Introduction
- Overview | What is x | How it works
END_STRUCTURE
"""
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test content " * 100}
structure = report_generator._determine_report_structure(
findings, "test query"
)
assert len(structure) == 1
assert len(structure[0]["subsections"]) == 1
# Should preserve everything after the first pipe
assert structure[0]["subsections"][0]["name"] == "Overview"
assert (
"What is x | How it works"
in structure[0]["subsections"][0]["purpose"]
)
def test_pipe_at_start_of_purpose(self, report_generator):
"""'|| double pipe' handles empty before first pipe."""
response = """
STRUCTURE
1. Section
- || double pipe content
END_STRUCTURE
"""
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test content " * 100}
structure = report_generator._determine_report_structure(
findings, "test query"
)
assert len(structure) == 1
# Empty name before first pipe - should handle gracefully
if structure[0]["subsections"]:
subsection = structure[0]["subsections"][0]
# The name might be empty or trimmed
assert "name" in subsection
# The name might be empty or the whole thing might be skipped
def test_empty_purpose_after_pipe(self, report_generator):
"""'Overview |' uses default purpose."""
response = """
STRUCTURE
1. Section
- Overview |
END_STRUCTURE
"""
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test content " * 100}
structure = report_generator._determine_report_structure(
findings, "test query"
)
assert len(structure) == 1
subsection = structure[0]["subsections"][0]
assert subsection["name"] == "Overview"
# Empty purpose after pipe should be empty string
assert subsection["purpose"] == ""
def test_whitespace_only_after_pipe(self, report_generator):
"""'Overview | ' strips to empty, uses default."""
response = """
STRUCTURE
1. Section
- Overview |
END_STRUCTURE
"""
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test content " * 100}
structure = report_generator._determine_report_structure(
findings, "test query"
)
subsection = structure[0]["subsections"][0]
assert subsection["name"] == "Overview"
# Whitespace should be stripped
assert subsection["purpose"].strip() == ""
def test_special_chars_in_section_name(self, report_generator):
"""[Section 1] (Important) parsed correctly."""
response = """
STRUCTURE
1. [Section 1] (Important)
- Details | Explanation
END_STRUCTURE
"""
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test content " * 100}
structure = report_generator._determine_report_structure(
findings, "test query"
)
assert len(structure) == 1
assert "[Section 1] (Important)" in structure[0]["name"]
def test_unicode_in_section_names(self, report_generator):
"""Non-ASCII characters handled."""
response = """
STRUCTURE
1. Introducción
- Resumen | Descripción general
2. 日本語セクション
- 詳細 | 説明
END_STRUCTURE
"""
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test content " * 100}
structure = report_generator._determine_report_structure(
findings, "test query"
)
assert len(structure) == 2
assert "Introducción" in structure[0]["name"]
assert "日本語セクション" in structure[1]["name"]
def test_very_long_section_names(self, report_generator):
"""Names over 200 chars."""
long_name = "A" * 250
response = f"""
STRUCTURE
1. {long_name}
- Subsection | Purpose
END_STRUCTURE
"""
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test content " * 100}
structure = report_generator._determine_report_structure(
findings, "test query"
)
assert len(structure) == 1
assert len(structure[0]["name"]) > 200
def test_numbered_section_with_leading_whitespace(self, report_generator):
"""' 1. Intro' parsed correctly."""
response = """
STRUCTURE
1. Introduction
- Overview | Purpose
END_STRUCTURE
"""
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test content " * 100}
structure = report_generator._determine_report_structure(
findings, "test query"
)
assert len(structure) == 1
assert "Introduction" in structure[0]["name"]
def test_subsection_without_dash(self, report_generator):
"""Missing dash marker handled."""
response = """
STRUCTURE
1. Section
Subsection without dash | Purpose
END_STRUCTURE
"""
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test content " * 100}
structure = report_generator._determine_report_structure(
findings, "test query"
)
# Subsection without dash should not be parsed
assert len(structure) == 1
# May have empty subsections list or no subsections
assert len(structure[0]["subsections"]) == 0
def test_consecutive_pipe_characters(self, report_generator):
"""'Name|||Purpose' handles multiple pipes."""
response = """
STRUCTURE
1. Section
- Name|||Purpose with pipes
END_STRUCTURE
"""
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test content " * 100}
structure = report_generator._determine_report_structure(
findings, "test query"
)
subsection = structure[0]["subsections"][0]
assert subsection["name"] == "Name"
# Everything after first pipe preserved
assert "||Purpose with pipes" in subsection["purpose"]
class TestMalformedStructureHandling:
"""Tests for handling malformed LLM responses."""
@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
from local_deep_research.report_generator import (
IntegratedReportGenerator,
)
generator = IntegratedReportGenerator(llm=mock_llm)
yield generator
def test_missing_structure_keyword(self, report_generator):
"""No STRUCTURE marker in response."""
response = """
1. Introduction
- Overview | Purpose
2. Main Content
- Details | Information
"""
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test content " * 100}
structure = report_generator._determine_report_structure(
findings, "test query"
)
# Should still parse numbered sections
assert len(structure) >= 0 # Might be empty or partially parsed
def test_missing_end_structure(self, report_generator):
"""No END_STRUCTURE marker."""
response = """
STRUCTURE
1. Introduction
- Overview | Purpose
2. Main Content
- Details | Information
"""
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test content " * 100}
structure = report_generator._determine_report_structure(
findings, "test query"
)
# Should still parse content after STRUCTURE
assert len(structure) == 2
def test_empty_structure_block(self, report_generator):
"""STRUCTURE...END_STRUCTURE with nothing between."""
response = """
STRUCTURE
END_STRUCTURE
"""
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test content " * 100}
structure = report_generator._determine_report_structure(
findings, "test query"
)
# Should return empty structure
assert structure == []
def test_invalid_json_like_structure(self, report_generator):
"""LLM returns JSON instead of expected format."""
response = """
STRUCTURE
{
"sections": [
{"name": "Introduction", "subsections": []}
]
}
END_STRUCTURE
"""
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test content " * 100}
structure = report_generator._determine_report_structure(
findings, "test query"
)
# Should not crash, might return empty
assert isinstance(structure, list)
def test_partial_section_definition(self, report_generator):
"""Incomplete section definition."""
response = """
STRUCTURE
1. Complete Section
- Subsection | Purpose
2.
3. Another Complete
- Sub | Purpose
END_STRUCTURE
"""
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test content " * 100}
structure = report_generator._determine_report_structure(
findings, "test query"
)
# Should parse complete sections, skip incomplete
# Section 2 has no name after the dot
assert len(structure) >= 2
def test_determine_structure_handles_string_response(
self, report_generator
):
"""A raw string LLM return (no .content) is handled without AttributeError."""
report_generator.model.invoke.return_value = (
"STRUCTURE\n1. Introduction\n - Overview | Purpose\nEND_STRUCTURE"
)
findings = {"current_knowledge": "Test content " * 100}
structure = report_generator._determine_report_structure(
findings, "test query"
)
assert len(structure) == 1
assert structure[0]["name"] == "Introduction"
def test_determine_structure_strips_think_tags(self, report_generator):
"""<think> reasoning is stripped before parsing so it can't create bogus sections."""
response = (
"<think>\n"
"1. First I should consider the topic\n"
"2. Then organize by theme\n"
"</think>\n"
"STRUCTURE\n"
"1. Introduction\n"
" - Overview | Purpose\n"
"END_STRUCTURE"
)
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test content " * 100}
structure = report_generator._determine_report_structure(
findings, "test query"
)
# Without stripping, the numbered lines inside <think> would parse as
# bogus sections; with stripping only the real section remains.
assert len(structure) == 1
assert structure[0]["name"] == "Introduction"
class TestSourceSectionFiltering:
"""Tests for filtering source-related 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
from local_deep_research.report_generator import (
IntegratedReportGenerator,
)
generator = IntegratedReportGenerator(llm=mock_llm)
yield generator
def test_references_section_removed(self, report_generator):
"""'References' as last section removed."""
response = """
STRUCTURE
1. Introduction
- Overview | Purpose
2. Main Content
- Details | Information
3. References
- List | Sources used
END_STRUCTURE
"""
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test content " * 100}
structure = report_generator._determine_report_structure(
findings, "test query"
)
# References should be removed
assert len(structure) == 2
assert all("References" not in s["name"] for s in structure)
def test_bibliography_section_removed(self, report_generator):
"""'Bibliography' as last section removed."""
response = """
STRUCTURE
1. Introduction
- Overview | Purpose
2. Bibliography
- Sources | References
END_STRUCTURE
"""
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test content " * 100}
structure = report_generator._determine_report_structure(
findings, "test query"
)
assert len(structure) == 1
assert "Bibliography" not in structure[0]["name"]
def test_sources_section_not_last_preserved(self, report_generator):
"""'Sources' not at end preserved."""
response = """
STRUCTURE
1. Data Sources
- Overview | Where data comes from
2. Analysis
- Details | Information
3. Conclusion
- Summary | Final thoughts
END_STRUCTURE
"""
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test content " * 100}
structure = report_generator._determine_report_structure(
findings, "test query"
)
# Data Sources is not last, should be preserved
# Only the LAST section is checked for source keywords
assert len(structure) == 3
assert any("Sources" in s["name"] for s in structure)
def test_multiple_source_sections(self, report_generator):
"""Only last source-related section removed."""
response = """
STRUCTURE
1. Data Sources Overview
- Types | Different sources
2. Main Content
- Details | Information
3. Citation Sources
- List | All citations
END_STRUCTURE
"""
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test content " * 100}
structure = report_generator._determine_report_structure(
findings, "test query"
)
# Only last section (Citation Sources) should be removed
assert len(structure) == 2
# First sources section should still be there
assert "Data Sources Overview" in structure[0]["name"]
def test_case_insensitive_source_detection(self, report_generator):
"""'REFERENCES', 'references' both detected."""
# Test uppercase
response = """
STRUCTURE
1. Introduction
- Overview | Purpose
2. REFERENCES
- List | Sources
END_STRUCTURE
"""
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test content " * 100}
structure = report_generator._determine_report_structure(
findings, "test query"
)
assert len(structure) == 1
assert "REFERENCES" not in structure[0]["name"]
class TestReportGenerationIntegration:
"""Integration tests for report generation flow."""
@pytest.fixture
def report_generator(self):
"""Create a report generator with mocked dependencies."""
with patch(
"local_deep_research.report_generator.AdvancedSearchSystem"
) as mock_search:
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
mock_search_instance = MagicMock()
mock_search_instance.all_links_of_system = []
mock_search_instance.analyze_topic.return_value = {
"current_knowledge": "Section content"
}
mock_search.return_value = mock_search_instance
from local_deep_research.report_generator import (
IntegratedReportGenerator,
)
generator = IntegratedReportGenerator(llm=mock_llm)
generator.search_system = mock_search_instance
yield generator
def test_section_without_subsections_creates_default(
self, report_generator
):
"""Section with no subsections gets default subsection."""
response = """
STRUCTURE
1. Standalone Section
END_STRUCTURE
"""
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test content " * 100}
structure = report_generator._determine_report_structure(
findings, "test query"
)
assert len(structure) == 1
assert structure[0]["subsections"] == [] # No subsections in structure
def test_research_and_generate_handles_empty_subsections(
self, report_generator
):
"""_research_and_generate_sections handles sections with no subsections."""
structure = [{"name": "Standalone", "subsections": []}]
# This should create default subsections during generation
sections = report_generator._research_and_generate_sections(
{"current_knowledge": "test", "questions_by_iteration": {}},
structure,
"test query",
)
assert "Standalone" in sections
class TestSplitDotBug:
"""Section-name parsing splits only on the first period (split('.', 1)).
The parser does: section_name = line.split(".", 1)[1].strip(), so section
names containing periods (e.g. "1. U.S. Policy") are preserved intact.
"""
@pytest.fixture
def report_generator(self):
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
from local_deep_research.report_generator import (
IntegratedReportGenerator,
)
generator = IntegratedReportGenerator(llm=mock_llm)
yield generator
def test_period_in_section_name_preserved(self, report_generator):
"""'1. U.S. Policy' keeps periods after the section number (split('.', 1))."""
response = "STRUCTURE\n1. U.S. Policy\nEND_STRUCTURE"
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test " * 100}
structure = report_generator._determine_report_structure(findings, "q")
assert len(structure) == 1
assert structure[0]["name"] == "U.S. Policy"
def test_abbreviation_in_section_name_preserved(self, report_generator):
"""'1. IPv4 vs. IPv6' keeps the trailing abbreviation (split('.', 1))."""
response = "STRUCTURE\n1. IPv4 vs. IPv6\nEND_STRUCTURE"
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test " * 100}
structure = report_generator._determine_report_structure(findings, "q")
assert structure[0]["name"] == "IPv4 vs. IPv6"
def test_double_digit_section_number(self, report_generator):
"""'10. Conclusion' parses correctly since split('.')[1] = ' Conclusion'."""
response = "STRUCTURE\n10. Conclusion\nEND_STRUCTURE"
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test " * 100}
structure = report_generator._determine_report_structure(findings, "q")
assert structure[0]["name"] == "Conclusion"
def test_orphan_subsection_before_section(self, report_generator):
"""Subsection before any section header is skipped."""
response = (
"STRUCTURE\n - Orphan | purpose\n1. Real Section\nEND_STRUCTURE"
)
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test " * 100}
structure = report_generator._determine_report_structure(findings, "q")
assert len(structure) == 1
assert structure[0]["name"] == "Real Section"
assert structure[0]["subsections"] == []
def test_only_source_section_returns_empty(self, report_generator):
"""If only section is source-related, result is empty."""
response = "STRUCTURE\n1. References\nEND_STRUCTURE"
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test " * 100}
structure = report_generator._determine_report_structure(findings, "q")
assert len(structure) == 0
def test_empty_lines_between_sections(self, report_generator):
"""Empty lines between sections are ignored."""
response = "STRUCTURE\n1. Intro\n\n\n2. Methods\nEND_STRUCTURE"
report_generator.model.invoke.return_value = MagicMock(content=response)
findings = {"current_knowledge": "Test " * 100}
structure = report_generator._determine_report_structure(findings, "q")
assert len(structure) == 2
class TestFormatFinalReport:
"""Tests for final report formatting."""
@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_instance = MagicMock()
mock_search_instance.all_links_of_system = []
from local_deep_research.report_generator import (
IntegratedReportGenerator,
)
generator = IntegratedReportGenerator(llm=mock_llm)
generator.search_system = mock_search_instance
yield generator
def test_format_includes_toc(self, report_generator):
"""Final report includes table of contents."""
structure = [
{
"name": "Introduction",
"subsections": [
{"name": "Overview", "purpose": "General intro"}
],
}
]
sections = {"Introduction": "# Introduction\n\nContent here"}
with patch(
"local_deep_research.report_generator.importlib"
) as mock_import:
mock_utils = MagicMock()
mock_utils.search_utilities.format_links_to_markdown.return_value = ""
mock_import.import_module.return_value = mock_utils
report = report_generator._format_final_report(
sections, structure, "test query"
)
assert "Table of Contents" in report["content"]
assert "Introduction" in report["content"]
def test_format_includes_metadata(self, report_generator):
"""Final report includes metadata."""
structure = []
sections = {}
with patch(
"local_deep_research.report_generator.importlib"
) as mock_import:
mock_utils = MagicMock()
mock_utils.search_utilities.format_links_to_markdown.return_value = ""
mock_import.import_module.return_value = mock_utils
report = report_generator._format_final_report(
sections, structure, "test query"
)
assert "metadata" in report
assert "generated_at" in report["metadata"]
assert "query" in report["metadata"]
assert report["metadata"]["query"] == "test query"