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
523 lines
18 KiB
Python
523 lines
18 KiB
Python
"""
|
|
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"]
|