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

176 lines
7.2 KiB
Python

"""Tests for ReportGenerator helper methods.
Covers _truncate_at_sentence_boundary and _build_previous_context — two
pure-logic helpers with zero prior test coverage that are critical for
report quality and repetition avoidance.
"""
from unittest.mock import MagicMock
import pytest
from local_deep_research.report_generator import IntegratedReportGenerator
@pytest.fixture
def generator():
"""Create an IntegratedReportGenerator with mocked dependencies."""
mock_llm = MagicMock()
mock_search = MagicMock()
gen = IntegratedReportGenerator.__new__(IntegratedReportGenerator)
gen.llm = mock_llm
gen.search_system = mock_search
gen.max_context_sections = 3
gen.max_context_chars = 4000
return gen
# ── _truncate_at_sentence_boundary ──
class TestTruncateAtSentenceBoundary:
"""Tests for _truncate_at_sentence_boundary."""
def test_text_shorter_than_limit_returned_unchanged(self, generator):
text = "Short text."
assert generator._truncate_at_sentence_boundary(text, 100) == text
def test_text_exactly_at_limit_returned_unchanged(self, generator):
text = "Exact." + "x" * 94 # 100 chars
assert generator._truncate_at_sentence_boundary(text, 100) == text
def test_truncates_at_period_followed_by_space(self, generator):
text = "First sentence. Second sentence. Third sentence that goes on."
result = generator._truncate_at_sentence_boundary(text, 35)
assert result.startswith("First sentence. Second sentence.")
assert result.endswith("\n[...truncated]")
def test_truncates_at_exclamation_mark(self, generator):
text = "Wow! This is amazing! More content follows here."
result = generator._truncate_at_sentence_boundary(text, 25)
assert "Wow! This is amazing!" in result
assert "[...truncated]" in result
def test_truncates_at_question_mark(self, generator):
text = "Is this working? Yes it is working perfectly fine here."
result = generator._truncate_at_sentence_boundary(text, 20)
assert "Is this working?" in result
assert "[...truncated]" in result
def test_boundary_at_end_of_truncated_text(self, generator):
# Period at exactly the last position of truncated text
text = "Hello." + "x" * 100
result = generator._truncate_at_sentence_boundary(text, 6)
# "Hello." is 6 chars, truncated[:6] = "Hello."
# last_sentence_end = 6, min_acceptable = int(6*0.8)=4, 6 > 4 → use boundary
assert "Hello." in result
assert "[...truncated]" in result
def test_no_sentence_boundary_falls_back_to_hard_truncation(
self, generator
):
text = "a" * 200
result = generator._truncate_at_sentence_boundary(text, 100)
assert result == "a" * 100 + "\n[...truncated]"
def test_sentence_boundary_too_early_falls_back(self, generator):
# Period at position 5 out of 100 → below 80% threshold
text = "Hi. " + "x" * 200
result = generator._truncate_at_sentence_boundary(text, 100)
# min_acceptable = 80, last_sentence_end = 4, 4 < 80 → hard truncation
assert result == text[:100] + "\n[...truncated]"
def test_sentence_boundary_at_80_percent_threshold(self, generator):
# Exactly at 80% boundary
# max_chars=100, min_acceptable=80
text = "x" * 80 + ". " + "y" * 50
result = generator._truncate_at_sentence_boundary(text, 100)
# last_sentence_end=81, min_acceptable=80, 81 > 80 → use boundary
assert result.endswith("\n[...truncated]")
assert result.startswith("x" * 80 + ".")
def test_period_followed_by_newline(self, generator):
text = "First sentence.\nSecond sentence continues for a while here."
result = generator._truncate_at_sentence_boundary(text, 20)
assert "First sentence." in result
assert "[...truncated]" in result
def test_period_not_followed_by_space_or_newline_ignored(self, generator):
# "3.14" has a period but it's followed by a digit, not space
text = "The value is 3.14 and more text follows after that point."
result = generator._truncate_at_sentence_boundary(text, 20)
# Only sentence boundaries followed by space/newline are valid
# In "The value is 3.14 a", the period at index 14 is followed by '1', not space
# So falls back to hard truncation
assert result == text[:20] + "\n[...truncated]"
def test_empty_text(self, generator):
assert generator._truncate_at_sentence_boundary("", 100) == ""
def test_single_character(self, generator):
assert generator._truncate_at_sentence_boundary("a", 100) == "a"
def test_multiple_sentence_endings_uses_last_valid(self, generator):
text = "One. Two. Three. Four. Five. Six. Seven. Eight."
result = generator._truncate_at_sentence_boundary(text, 30)
# Should find the last boundary within the first 30 chars
# "One. Two. Three. Four. Five. " is 29 chars
assert "[...truncated]" in result
# ── _build_previous_context ──
class TestBuildPreviousContext:
"""Tests for _build_previous_context."""
def test_empty_list_returns_empty_string(self, generator):
assert generator._build_previous_context([]) == ""
def test_single_finding_included(self, generator):
result = generator._build_previous_context(["Finding 1"])
assert "Finding 1" in result
assert "DO NOT REPEAT" in result
assert "CONTENT ALREADY WRITTEN" in result
def test_respects_max_context_sections_limit(self, generator):
generator.max_context_sections = 2
findings = ["Finding 1", "Finding 2", "Finding 3", "Finding 4"]
result = generator._build_previous_context(findings)
# Should only include last 2 findings
assert "Finding 3" in result
assert "Finding 4" in result
assert "Finding 1" not in result
assert "Finding 2" not in result
def test_joins_with_separator(self, generator):
result = generator._build_previous_context(["A", "B"])
assert "\n\n---\n\n" in result
def test_truncates_long_context(self, generator):
generator.max_context_chars = 50
long_finding = "x" * 100
result = generator._build_previous_context([long_finding])
# The content should be truncated
assert "[...truncated]" in result
def test_formatting_markers_present(self, generator):
result = generator._build_previous_context(["Test content"])
assert "=== CONTENT ALREADY WRITTEN (DO NOT REPEAT) ===" in result
assert "=== END OF PREVIOUS CONTENT ===" in result
assert "CRITICAL:" in result
def test_context_within_char_limit_not_truncated(self, generator):
generator.max_context_chars = 10000
result = generator._build_previous_context(["Short finding."])
assert "[...truncated]" not in result
def test_uses_last_n_sections(self, generator):
generator.max_context_sections = 3
findings = [f"Finding {i}" for i in range(10)]
result = generator._build_previous_context(findings)
assert "Finding 7" in result
assert "Finding 8" in result
assert "Finding 9" in result
assert "Finding 0" not in result