chore: import upstream snapshot with attribution
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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:08:55 +08:00
commit 7a0da7932b
2985 changed files with 1049377 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Tests for text optimization module."""
@@ -0,0 +1,101 @@
"""Edge-case tests for citation exporters — math mode, list EOF, BibTeX URL, and RIS unicode."""
class TestInlineMathPreservedBetweenDollars:
"""Verify content between $ markers is NOT escaped during LaTeX export."""
def test_inline_math_preserved_between_dollars(self):
"""$x^2 + y^2$ should keep ^ and + unescaped inside math mode."""
from local_deep_research.text_optimization.citation_formatter import (
LaTeXExporter,
)
exporter = LaTeXExporter()
content = "# Title\n\nThe formula $x^2 + y^2 = r^2$ is important."
result = exporter.export_to_latex(content)
# Math content between $ should be preserved (not escaped)
assert "$x^2 + y^2 = r^2$" in result
class TestDisplayMathDoubleDollarPreserved:
"""Verify $$...$$ display math blocks are preserved during LaTeX export."""
def test_display_math_double_dollar_preserved(self):
"""$$E = mc^2$$ should preserve the math content unescaped."""
from local_deep_research.text_optimization.citation_formatter import (
LaTeXExporter,
)
exporter = LaTeXExporter()
content = "# Title\n\n$$E = mc^2$$\n\nSome text."
result = exporter.export_to_latex(content)
# The display math content should be preserved
assert "E = mc^2" in result
# The ^ should NOT be escaped to \textasciicircum{} inside math
assert r"\textasciicircum" not in result
class TestListAtEndOfContentGetsClosingTag:
"""Verify list items at EOF trigger \\end{itemize} via _convert_lists lines 940-941."""
def test_list_at_end_of_content_gets_closing_tag(self):
"""List items at end of content (no trailing text) should get \\end{itemize}."""
from local_deep_research.text_optimization.citation_formatter import (
LaTeXExporter,
)
exporter = LaTeXExporter()
# Content ends with list items, no trailing non-list text
content = "# Title\n\nSome intro text.\n\n- Item one\n- Item two\n- Item three"
result = exporter.export_to_latex(content)
begin_count = result.count("\\begin{itemize}")
end_count = result.count("\\end{itemize}")
assert begin_count == end_count == 1
# The last \end{itemize} should be after the last \item
last_item_pos = result.rfind("\\item")
last_end_pos = result.rfind("\\end{itemize}")
assert last_end_pos > last_item_pos
class TestSourceWithoutUrlOmitsBibtexUrlField:
"""Verify BibTeX entry omits url= field when source has no URL."""
def test_source_without_url_omits_bibtex_url_field(self):
"""[1] Title without URL line → BibTeX entry should not contain url = {...}."""
from local_deep_research.text_optimization.citation_formatter import (
QuartoExporter,
)
exporter = QuartoExporter()
# Source without URL line
content = "# Report\n\nSome text [1].\n\n## Sources\n[1] A Book Title Without URL"
bib_content = exporter._generate_bibliography(content)
assert "ref1" in bib_content
assert "A Book Title Without URL" in bib_content
# Should NOT have url field since no URL was provided
assert "url = " not in bib_content
assert "howpublished" not in bib_content
class TestRisExporterHandlesUnicodeTitle:
"""Verify RIS exporter preserves Unicode characters in TI field."""
def test_ris_exporter_handles_unicode_title(self):
"""Unicode title '日本語' in RIS TI field should be preserved."""
from local_deep_research.text_optimization.citation_formatter import (
RISExporter,
)
exporter = RISExporter()
content = (
"## Sources\n[1] 日本語の研究論文\n URL: https://example.jp/paper"
)
ris = exporter.export_to_ris(content)
assert "TI - " in ris
assert "日本語" in ris
assert "UR - https://example.jp/paper" in ris
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,167 @@
"""Edge-case tests for citation_formatter — idempotency, parsing, and export boundaries."""
def _make_document(body, sources):
"""Helper: combine body text with a sources section."""
return body + "\n\n## Sources\n" + sources
class TestAlreadyFormattedCitations:
"""Ensure already-formatted citations are not double-processed."""
def test_already_formatted_not_rematched(self):
"""[[1]](url) should NOT be re-formatted to [[[1]]](url)."""
from local_deep_research.text_optimization.citation_formatter import (
CitationFormatter,
CitationMode,
)
formatter = CitationFormatter(CitationMode.NUMBER_HYPERLINKS)
doc = _make_document(
"See [[1]](https://example.com) for details.",
"[1] Example Source\n URL: https://example.com",
)
result = formatter.format_document(doc)
# The negative lookbehind (?<![\[) should prevent matching inside [[ ]]
assert "[[[1]]]" not in result
def test_format_document_idempotent(self):
"""Calling format_document twice produces identical output."""
from local_deep_research.text_optimization.citation_formatter import (
CitationFormatter,
CitationMode,
)
formatter = CitationFormatter(CitationMode.NUMBER_HYPERLINKS)
doc = _make_document(
"See [1] for details.",
"[1] Example Source\n URL: https://example.com",
)
first = formatter.format_document(doc)
second = formatter.format_document(first)
assert first == second
class TestSourceParsing:
"""Tests for _parse_sources edge cases."""
def test_parse_sources_tab_indented_url(self):
"""Tab indentation in \\tURL: should be parsed."""
from local_deep_research.text_optimization.citation_formatter import (
CitationFormatter,
CitationMode,
)
formatter = CitationFormatter(CitationMode.NUMBER_HYPERLINKS)
sources_text = "## Sources\n[1] Title here\n\tURL: https://example.com"
sources = formatter._parse_sources(sources_text)
# The regex uses \\s* which matches tabs
assert "1" in sources
_, url = sources["1"]
assert url == "https://example.com"
class TestDomainIdAssignment:
"""Tests for domain ID assignment order."""
def test_domain_id_assignment_preserves_source_order(self):
"""Source 1 gets -1, source 5 gets -2 deterministically for same domain."""
from local_deep_research.text_optimization.citation_formatter import (
CitationFormatter,
CitationMode,
)
formatter = CitationFormatter(CitationMode.DOMAIN_ID_ALWAYS_HYPERLINKS)
doc = _make_document(
"See [1] and [5].",
"[1] First Article\n URL: https://arxiv.org/abs/1234\n"
"[5] Second Article\n URL: https://arxiv.org/abs/5678",
)
result = formatter.format_document(doc)
# Both are from arxiv.org, should get -1 and -2
assert "arxiv.org-1" in result
assert "arxiv.org-2" in result
class TestRISExporterBoundary:
"""Tests for RIS exporter deduplication boundary."""
def test_ris_exporter_stops_at_all_sources(self):
"""Deduplication boundary at '## ALL SOURCES' section."""
from local_deep_research.text_optimization.citation_formatter import (
RISExporter,
)
exporter = RISExporter()
content = (
"## Sources\n"
"[1] First Source\n URL: https://example.com\n\n"
"## ALL SOURCES\n"
"[1] First Source (duplicate)\n URL: https://example.com\n"
"[2] Second Source\n URL: https://other.com"
)
ris = exporter.export_to_ris(content)
# Should only include source [1] from before the ALL SOURCES marker
assert "ref1" in ris
assert "ref2" not in ris
class TestLaTeXListBalance:
"""Tests for LaTeX list conversion."""
def test_latex_list_balanced_with_empty_lines(self):
"""\\begin{itemize} and \\end{itemize} are always balanced."""
from local_deep_research.text_optimization.citation_formatter import (
LaTeXExporter,
)
exporter = LaTeXExporter()
content = (
"# Title\n\n- Item one\n- Item two\n\nSome text\n\n- Item three\n"
)
result = exporter.export_to_latex(content)
begin_count = result.count("\\begin{itemize}")
end_count = result.count("\\end{itemize}")
assert begin_count == end_count
assert begin_count >= 1
class TestSourceWordPattern:
"""Tests for 'Source X' pattern matching."""
def test_source_word_with_trailing_period(self):
"""'Source 1.' at end-of-sentence converts correctly."""
from local_deep_research.text_optimization.citation_formatter import (
CitationFormatter,
CitationMode,
)
formatter = CitationFormatter(CitationMode.NUMBER_HYPERLINKS)
doc = _make_document(
"According to Source 1.",
"[1] Example\n URL: https://example.com",
)
result = formatter.format_document(doc)
# "Source 1" should be replaced; the period remains
assert "Source 1" not in result.split("## Sources")[0]
def test_comma_citations_with_spaces(self):
"""[1, 2, 3] with varying space patterns."""
from local_deep_research.text_optimization.citation_formatter import (
CitationFormatter,
CitationMode,
)
formatter = CitationFormatter(CitationMode.NUMBER_HYPERLINKS)
doc = _make_document(
"See [1, 2, 3] for details.",
"[1] Source A\n URL: https://a.com\n"
"[2] Source B\n URL: https://b.com\n"
"[3] Source C\n URL: https://c.com",
)
result = formatter.format_document(doc)
body = result.split("## Sources")[0]
# Each citation should be individually formatted
assert "[[1]]" in body
assert "[[2]]" in body
assert "[[3]]" in body
@@ -0,0 +1,380 @@
"""High-value pure logic tests for citation_formatter.py."""
import pytest
from local_deep_research.text_optimization.citation_formatter import (
CitationFormatter,
CitationMode,
LaTeXExporter,
QuartoExporter,
RISExporter,
find_sources_section,
)
# ---------------------------------------------------------------------------
# CitationMode enum
# ---------------------------------------------------------------------------
class TestCitationMode:
def test_enum_values(self):
assert CitationMode.NUMBER_HYPERLINKS.value == "number_hyperlinks"
assert CitationMode.DOMAIN_HYPERLINKS.value == "domain_hyperlinks"
assert CitationMode.DOMAIN_ID_HYPERLINKS.value == "domain_id_hyperlinks"
assert (
CitationMode.DOMAIN_ID_ALWAYS_HYPERLINKS.value
== "domain_id_always_hyperlinks"
)
assert (
CitationMode.SOURCE_TAGGED_HYPERLINKS.value
== "source_tagged_hyperlinks"
)
assert CitationMode.NO_HYPERLINKS.value == "no_hyperlinks"
def test_enum_member_count(self):
# NUMBER_HYPERLINKS, DOMAIN_HYPERLINKS, DOMAIN_ID_HYPERLINKS,
# DOMAIN_ID_ALWAYS_HYPERLINKS, SOURCE_TAGGED_HYPERLINKS, NO_HYPERLINKS
assert len(CitationMode) == 6
# ---------------------------------------------------------------------------
# find_sources_section
# ---------------------------------------------------------------------------
class TestFindSourcesSection:
def test_markdown_heading_sources(self):
text = "Some text\n## Sources\n[1] Foo"
assert find_sources_section(text) == text.index("## Sources")
def test_markdown_heading_references(self):
text = "Body\n# References\n[1] Bar"
assert find_sources_section(text) == text.index("# References")
def test_plain_label_sources(self):
text = "Body paragraph\nSources:\n[1] Item"
assert find_sources_section(text) == text.index("Sources:")
def test_case_insensitive(self):
text = "Body\n## BIBLIOGRAPHY\n[1] X"
assert find_sources_section(text) != -1
def test_no_section_returns_negative_one(self):
assert find_sources_section("No references here.") == -1
def test_empty_string(self):
assert find_sources_section("") == -1
def test_citations_heading_detected(self):
text = "Content\n### Citations\nSome refs"
assert find_sources_section(text) == text.index("### Citations")
# ---------------------------------------------------------------------------
# CitationFormatter._extract_domain
# ---------------------------------------------------------------------------
class TestExtractDomain:
def setup_method(self):
self.fmt = CitationFormatter()
def test_known_domain_arxiv(self):
assert (
self.fmt._extract_domain("https://arxiv.org/abs/1234")
== "arxiv.org"
)
def test_known_domain_github(self):
assert (
self.fmt._extract_domain("https://github.com/user/repo")
== "github.com"
)
def test_www_prefix_stripped(self):
assert (
self.fmt._extract_domain("https://www.example.com/page")
== "example.com"
)
def test_subdomain_returns_main_domain(self):
assert (
self.fmt._extract_domain("https://docs.python.org/3/")
== "python.org"
)
def test_empty_string_returns_empty(self):
# urlparse("") produces empty netloc; no exception is raised
assert self.fmt._extract_domain("") == ""
def test_no_scheme_url_returns_empty(self):
# urlparse without scheme puts everything in path, netloc is empty
assert self.fmt._extract_domain("not-a-url") == ""
def test_none_url_raises(self):
# None triggers TypeError which is not caught by the handler
with pytest.raises(TypeError):
self.fmt._extract_domain(None)
# ---------------------------------------------------------------------------
# CitationFormatter._parse_sources
# ---------------------------------------------------------------------------
class TestParseSources:
def setup_method(self):
self.fmt = CitationFormatter()
def test_basic_source_with_url(self):
text = "[1] My Title\n URL: https://example.com"
result = self.fmt._parse_sources(text)
assert "1" in result
assert result["1"] == ("My Title", "https://example.com")
def test_source_without_url(self):
text = "[2] Title Only"
result = self.fmt._parse_sources(text)
assert "2" in result
assert result["2"][1] == ""
def test_multiple_sources(self):
text = "[1] First\n[2] Second"
result = self.fmt._parse_sources(text)
assert len(result) == 2
def test_empty_string(self):
result = self.fmt._parse_sources("")
assert result == {}
def test_comma_separated_citation_numbers(self):
text = "[3, 4] Shared Title\n URL: https://shared.com"
result = self.fmt._parse_sources(text)
assert "3" in result
assert "4" in result
assert result["3"][0] == "Shared Title"
# ---------------------------------------------------------------------------
# CitationFormatter._replace_comma_citations
# ---------------------------------------------------------------------------
class TestReplaceCommaCitations:
def setup_method(self):
self.fmt = CitationFormatter()
def test_comma_separated_replaced(self):
lookup = {"1": ("T1", "http://a.com"), "2": ("T2", "http://b.com")}
def format_one(num, data):
_, url = data
return f"[[{num}]]({url})"
result = self.fmt._replace_comma_citations(
"See [1, 2] here", lookup, format_one
)
assert "[[1]](http://a.com)" in result
assert "[[2]](http://b.com)" in result
assert "[1, 2]" not in result
def test_missing_citation_falls_back(self):
lookup = {"1": ("T1", "http://a.com")}
def format_one(num, data):
return f"[{num}]"
result = self.fmt._replace_comma_citations(
"See [1, 99] here", lookup, format_one
)
assert "[99]" in result
# ---------------------------------------------------------------------------
# CitationFormatter._format_number_hyperlinks
# ---------------------------------------------------------------------------
class TestFormatNumberHyperlinks:
def setup_method(self):
self.fmt = CitationFormatter()
def test_basic_hyperlink(self):
sources = {"1": ("Title", "https://example.com")}
result = self.fmt._format_number_hyperlinks(
"See [1] for details.", sources
)
assert "[[1]](https://example.com)" in result
def test_source_without_url_unchanged(self):
sources = {"1": ("Title", "")}
result = self.fmt._format_number_hyperlinks(
"See [1] for details.", sources
)
assert "[1]" in result
assert "]()" not in result
def test_source_word_pattern(self):
sources = {"1": ("Title", "https://example.com")}
result = self.fmt._format_number_hyperlinks(
"According to Source 1.", sources
)
assert "[[1]](https://example.com)" in result
# ---------------------------------------------------------------------------
# CitationFormatter._format_domain_id_hyperlinks
# ---------------------------------------------------------------------------
class TestFormatDomainIdHyperlinks:
def setup_method(self):
self.fmt = CitationFormatter()
def test_single_domain_no_id_suffix(self):
sources = {"1": ("Title", "https://arxiv.org/abs/123")}
result = self.fmt._format_domain_id_hyperlinks("See [1].", sources)
assert "[[arxiv.org]]" in result
def test_multiple_same_domain_gets_id(self):
sources = {
"1": ("A", "https://arxiv.org/abs/1"),
"2": ("B", "https://arxiv.org/abs/2"),
}
result = self.fmt._format_domain_id_hyperlinks(
"See [1] and [2].", sources
)
assert "arxiv.org-1" in result
assert "arxiv.org-2" in result
# ---------------------------------------------------------------------------
# LaTeXExporter._escape_latex
# ---------------------------------------------------------------------------
class TestLatexEscapeLatex:
def setup_method(self):
self.exporter = LaTeXExporter()
def test_ampersand(self):
assert self.exporter._escape_latex("A & B") == r"A \& B"
def test_percent(self):
assert self.exporter._escape_latex("100%") == r"100\%"
def test_underscore(self):
assert self.exporter._escape_latex("my_var") == r"my\_var"
def test_hash(self):
assert self.exporter._escape_latex("#1") == r"\#1"
def test_tilde(self):
assert r"\textasciitilde{}" in self.exporter._escape_latex("~")
def test_empty_string(self):
assert self.exporter._escape_latex("") == ""
def test_multiple_specials(self):
result = self.exporter._escape_latex("A & B % C")
assert r"\&" in result
assert r"\%" in result
# ---------------------------------------------------------------------------
# LaTeXExporter._convert_lists
# ---------------------------------------------------------------------------
class TestLatexConvertLists:
def setup_method(self):
self.exporter = LaTeXExporter()
def test_single_bullet(self):
result = self.exporter._convert_lists("- Hello")
assert r"\begin{itemize}" in result
assert r"\item Hello" in result
assert r"\end{itemize}" in result
def test_multiple_bullets(self):
result = self.exporter._convert_lists("- One\n- Two\n- Three")
assert result.count(r"\item") == 3
# Only one begin/end pair
assert result.count(r"\begin{itemize}") == 1
assert result.count(r"\end{itemize}") == 1
def test_no_bullets_unchanged(self):
text = "Just a paragraph."
result = self.exporter._convert_lists(text)
assert r"\begin{itemize}" not in result
def test_list_ends_on_non_list_line(self):
result = self.exporter._convert_lists("- Item\nParagraph text")
assert r"\end{itemize}" in result
# ---------------------------------------------------------------------------
# RISExporter basic entry formatting
# ---------------------------------------------------------------------------
class TestRISExporter:
def setup_method(self):
self.exporter = RISExporter()
def test_no_sources_section_returns_empty(self):
result = self.exporter.export_to_ris("No references here at all.")
assert result == ""
def test_basic_export_contains_ris_markers(self):
content = (
"Body text\n## Sources\n[1] My Paper\n URL: https://example.com"
)
result = self.exporter.export_to_ris(content)
assert "TY - ELEC" in result
assert "ER - " in result
assert "ID - ref1" in result
def test_url_included_in_entry(self):
content = "Text\n## Sources\n[1] Paper Title\n URL: https://example.com/paper"
result = self.exporter.export_to_ris(content)
assert "UR - https://example.com/paper" in result
# ---------------------------------------------------------------------------
# QuartoExporter output
# ---------------------------------------------------------------------------
class TestQuartoExporter:
def setup_method(self):
self.exporter = QuartoExporter()
def test_yaml_header_present(self):
content = "# My Report\nSome text [1]\n## Sources\n[1] Ref"
result = self.exporter.export_to_quarto(content)
assert result.startswith("---")
assert "title:" in result
assert "bibliography: references.bib" in result
def test_citation_converted_to_quarto_format(self):
content = "See [1] for details.\n## Sources\n[1] A paper"
result = self.exporter.export_to_quarto(content)
assert "[@ref1]" in result
def test_comma_citations_converted(self):
content = "See [1, 2] here.\n## Sources\n[1] A\n[2] B"
result = self.exporter.export_to_quarto(content)
assert "@ref1" in result
assert "@ref2" in result
def test_custom_title(self):
content = "Some body text"
result = self.exporter.export_to_quarto(content, title="Custom Title")
assert "Custom Title" in result
def test_title_extracted_from_heading(self):
content = "# Extracted Title\nBody"
result = self.exporter.export_to_quarto(content)
assert "Extracted Title" in result
+236
View File
@@ -0,0 +1,236 @@
"""Integration tests for text optimization with the research service."""
import pytest
from unittest.mock import patch
from local_deep_research.web.services.research_service import (
get_citation_formatter,
export_report_to_memory,
)
from local_deep_research.text_optimization import (
CitationFormatter,
CitationMode,
)
class TestResearchServiceIntegration:
"""Test integration of text optimization with research service."""
@patch("local_deep_research.config.search_config.get_setting_from_snapshot")
def test_get_citation_formatter_number_mode(self, mock_get_setting):
"""Test getting formatter with number hyperlinks mode."""
mock_get_setting.return_value = "number_hyperlinks"
formatter = get_citation_formatter()
assert isinstance(formatter, CitationFormatter)
assert formatter.mode == CitationMode.NUMBER_HYPERLINKS
@patch("local_deep_research.config.search_config.get_setting_from_snapshot")
def test_get_citation_formatter_domain_mode(self, mock_get_setting):
"""Test getting formatter with domain hyperlinks mode."""
mock_get_setting.return_value = "domain_hyperlinks"
formatter = get_citation_formatter()
assert isinstance(formatter, CitationFormatter)
assert formatter.mode == CitationMode.DOMAIN_HYPERLINKS
@patch("local_deep_research.config.search_config.get_setting_from_snapshot")
def test_get_citation_formatter_no_hyperlinks_mode(self, mock_get_setting):
"""Test getting formatter with no hyperlinks mode."""
mock_get_setting.return_value = "no_hyperlinks"
formatter = get_citation_formatter()
assert isinstance(formatter, CitationFormatter)
assert formatter.mode == CitationMode.NO_HYPERLINKS
@patch("local_deep_research.config.search_config.get_setting_from_snapshot")
def test_get_citation_formatter_invalid_mode(self, mock_get_setting):
"""Test getting formatter with invalid mode falls back to default."""
mock_get_setting.return_value = "invalid_mode"
formatter = get_citation_formatter()
assert isinstance(formatter, CitationFormatter)
assert formatter.mode == CitationMode.NUMBER_HYPERLINKS # Default
def test_export_report_to_latex(self):
"""Test LaTeX export functionality."""
markdown_content = """# Test Report
This is a test with citation [1].
## Sources
[1] Test Source
URL: https://example.com
"""
# Export to LaTeX
latex_bytes, filename, mimetype = export_report_to_memory(
markdown_content, "latex"
)
# Check export results
assert filename.endswith(".tex")
assert mimetype == "text/plain"
# Check LaTeX content
latex_content = latex_bytes.decode("utf-8")
assert r"\documentclass[12pt]{article}" in latex_content
assert r"\section{Test Report}" in latex_content
assert r"\cite{1}" in latex_content
assert r"\bibitem{1}" in latex_content
@patch("local_deep_research.config.search_config.get_setting_from_snapshot")
def test_real_world_citation_formatting(self, mock_get_setting):
"""Test citation formatting with real-world example."""
mock_get_setting.return_value = "number_hyperlinks"
# Simulate research report content
content = """# Deep Learning Research Summary
Query: What are the latest advances in transformer architectures?
## Executive Summary
Recent advances in transformer architectures have focused on efficiency improvements [1],
novel attention mechanisms [2], and scaling laws [3]. The field has seen rapid progress
with models like GPT-4 [4] and Claude [5] demonstrating impressive capabilities.
## Key Findings
### Efficiency Improvements
Multiple research groups have proposed methods to reduce computational complexity [1, 2, 3]:
- Flash Attention reduces memory usage significantly [1]
- Sparse transformers achieve O(n√n) complexity [2]
- Linear attention approximations show promise [3]
### Novel Architectures
Recent architectural innovations include:
- Mixture of Experts (MoE) models [4][5]
- Retrieval-augmented generation [6]
- Tool-use capabilities [7][8][9]
## Sources
[1] FlashAttention: Fast and Memory-Efficient Exact Attention
URL: https://arxiv.org/abs/2205.14135
[2] Efficient Transformers: A Survey
URL: https://arxiv.org/abs/2009.06732
[3] Linformer: Self-Attention with Linear Complexity
URL: https://arxiv.org/abs/2006.04768
[4] Mixtral of Experts
URL: https://arxiv.org/abs/2401.04088
[5] Switch Transformers
URL: https://arxiv.org/abs/2101.03961
[6] Retrieval-Augmented Generation
URL: https://arxiv.org/abs/2005.11401
[7] Toolformer: Language Models Can Teach Themselves
URL: https://arxiv.org/abs/2302.04761
[8] WebGPT: Browser-assisted question-answering
URL: https://arxiv.org/abs/2112.09332
[9] Constitutional AI: Harmlessness from AI Feedback
URL: https://arxiv.org/abs/2212.08073
"""
formatter = get_citation_formatter()
result = formatter.format_document(content)
# Verify all citation formats are handled correctly
assert "[[1]](https://arxiv.org/abs/2205.14135)" in result
assert (
"[[1]](https://arxiv.org/abs/2205.14135)[[2]](https://arxiv.org/abs/2009.06732)[[3]](https://arxiv.org/abs/2006.04768)"
in result
)
assert (
"[[4]](https://arxiv.org/abs/2401.04088)[[5]](https://arxiv.org/abs/2101.03961)"
in result
)
assert (
"[[7]](https://arxiv.org/abs/2302.04761)[[8]](https://arxiv.org/abs/2112.09332)[[9]](https://arxiv.org/abs/2212.08073)"
in result
)
# Ensure sources section is preserved
assert "## Sources" in result
assert "[1] FlashAttention" in result
def test_export_report_to_quarto(self):
"""Test Quarto export functionality."""
markdown_content = """# AI Research Report
This report discusses recent advances [1] and challenges [2].
## Sources
[1] Advances in AI
URL: https://arxiv.org/abs/2024.1234
[2] AI Challenges
URL: https://example.com/challenges
"""
# Export to Quarto
zip_bytes, filename, mimetype = export_report_to_memory(
markdown_content, "quarto", "AI Research"
)
# Check export results
assert filename.endswith(".zip")
assert mimetype == "application/zip"
# Extract and check content from zip
import zipfile
import io
zip_buffer = io.BytesIO(zip_bytes)
with zipfile.ZipFile(zip_buffer, "r") as zipf:
# Check files in zip
file_list = zipf.namelist()
qmd_files = [f for f in file_list if f.endswith(".qmd")]
bib_files = [f for f in file_list if f.endswith(".bib")]
assert len(qmd_files) == 1, "Should have one .qmd file"
assert len(bib_files) == 1, "Should have one .bib file"
# Check Quarto content
quarto_content = zipf.read(qmd_files[0]).decode("utf-8")
# Verify YAML header
assert 'title: "AI Research"' in quarto_content
assert "bibliography: references.bib" in quarto_content
# Verify citation conversion
assert "[@ref1]" in quarto_content
assert "[@ref2]" in quarto_content
# Check bibliography content
bib_content = zipf.read(bib_files[0]).decode("utf-8")
assert "@misc{ref1," in bib_content
assert 'title = "{Advances in AI}"' in bib_content
def test_export_with_invalid_format(self):
"""Test export with invalid format raises error."""
markdown_content = "# Test"
with pytest.raises(ValueError, match="Unsupported export format"):
export_report_to_memory(markdown_content, "invalid_format")
@patch("local_deep_research.config.search_config.get_setting_from_snapshot")
def test_automatic_export_formats(self, mock_get_setting):
"""Test automatic export to multiple formats based on settings."""
# This would be called in the actual research service when saving reports
mock_get_setting.return_value = ["markdown", "latex", "quarto"]
# Simulate the export logic
export_formats = mock_get_setting("report.export_formats", ["markdown"])
assert "markdown" in export_formats
assert "latex" in export_formats
assert "quarto" in export_formats
@@ -0,0 +1,490 @@
"""
Tests for RISExporter._create_ris_entry() edge cases.
Existing tests cover: basic ID, year extraction, DOI from metadata,
GitHub publisher, and arXiv publisher.
This file adds coverage for the many remaining branches:
- Author parsing (split by 'and', '&', commas)
- DOI extraction from text (not just metadata)
- Publisher detection for Reddit, YouTube, Medium, PyPI, generic domains
- www. prefix stripping
- Title cleaning (DOI, "Published in", "Volume", "Pages" patterns)
- Empty/minimal inputs
- Author match overlapping with title cleaning
"""
import re
from freezegun import freeze_time
from local_deep_research.text_optimization.citation_formatter import RISExporter
class TestRISEntryAuthorParsing:
"""Tests for author extraction from 'by Author1, Author2' patterns."""
def _make(self):
return RISExporter()
def test_single_author(self):
"""Extracts a single author."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Some Title\nWritten by Jane Doe."
)
assert "AU - Jane Doe" in result
def test_authors_separated_by_and(self):
"""Authors separated by 'and' are split correctly."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title\nby Alice Smith and Bob Jones."
)
assert "AU - Alice Smith" in result
assert "AU - Bob Jones" in result
def test_authors_separated_by_ampersand(self):
"""Authors separated by '&' are split correctly."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title\nby Alice Smith & Bob Jones."
)
assert "AU - Alice Smith" in result
assert "AU - Bob Jones" in result
def test_authors_separated_by_comma(self):
"""Authors separated by commas are split correctly."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title\nby Alice Smith, Bob Jones, Carol White."
)
assert "AU - Alice Smith" in result
assert "AU - Bob Jones" in result
assert "AU - Carol White" in result
def test_authors_mixed_separators(self):
"""Authors with mixed 'and' and comma separators."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title\nby Alice, Bob and Carol."
)
assert "AU - Alice" in result
assert "AU - Bob" in result
assert "AU - Carol" in result
def test_no_authors(self):
"""No author line produces no AU entries."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Title with no authors")
assert "AU -" not in result
def test_by_keyword_case_insensitive(self):
"""The 'by' keyword match is case-insensitive."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Title\nBY John Smith.")
assert "AU - John Smith" in result
def test_empty_author_parts_filtered(self):
"""Empty parts from splitting are filtered out."""
exporter = self._make()
# Trailing comma leaves an empty part
result = exporter._create_ris_entry("1", "Title\nby Alice, , Bob.")
lines = [
line for line in result.split("\n") if line.startswith("AU -")
]
# Should only have real authors, not empty strings
for line in lines:
assert line.strip() != "AU -"
class TestRISEntryDOIExtraction:
"""Tests for DOI extraction from metadata and text."""
def _make(self):
return RISExporter()
def test_doi_from_metadata_takes_precedence(self):
"""DOI from metadata dict is used over DOI in text."""
exporter = self._make()
result = exporter._create_ris_entry(
"1",
"Title DOI: 10.9999/text-doi",
metadata={"doi": "10.1234/meta-doi"},
)
assert "DO - 10.1234/meta-doi" in result
assert (
"10.9999/text-doi" not in result.split("DO - ")[1].split("\n")[0]
)
def test_doi_from_text(self):
"""DOI extracted from text when not in metadata."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title\nDOI: 10.1234/test.5678"
)
assert "DO - 10.1234/test.5678" in result
def test_doi_from_text_case_insensitive(self):
"""DOI extraction from text is case-insensitive."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title\ndoi: 10.1234/lowercase"
)
assert "DO - 10.1234/lowercase" in result
def test_no_doi(self):
"""No DOI produces no DO entry."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Title with no DOI")
assert "DO -" not in result
def test_metadata_none_falls_back_to_text(self):
"""When metadata is None, DOI is extracted from text."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Title\nDOI: 10.5555/fallback")
assert "DO - 10.5555/fallback" in result
def test_metadata_without_doi_key(self):
"""Metadata dict without 'doi' key falls back to text."""
exporter = self._make()
result = exporter._create_ris_entry(
"1",
"Title\nDOI: 10.7777/textdoi",
metadata={"other": "value"},
)
assert "DO - 10.7777/textdoi" in result
class TestRISEntryPublisherDetection:
"""Tests for publisher extraction from URL domains."""
def _make(self):
return RISExporter()
def test_reddit_publisher(self):
"""Reddit URL produces Reddit publisher."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title", url="https://www.reddit.com/r/python/post"
)
assert "PB - Reddit" in result
def test_reddit_subdomain(self):
"""Reddit subdomain also detected."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title", url="https://old.reddit.com/r/science"
)
assert "PB - Reddit" in result
def test_youtube_publisher(self):
"""YouTube URL produces YouTube publisher."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title", url="https://www.youtube.com/watch?v=abc123"
)
assert "PB - YouTube" in result
def test_youtube_mobile(self):
"""Mobile YouTube URL produces YouTube publisher."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title", url="https://m.youtube.com/watch?v=abc123"
)
assert "PB - YouTube" in result
def test_medium_publisher(self):
"""Medium URL produces Medium publisher."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title", url="https://medium.com/@author/article"
)
assert "PB - Medium" in result
def test_medium_subdomain(self):
"""Medium subdomain (custom publication) detected."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title", url="https://blog.medium.com/post"
)
assert "PB - Medium" in result
def test_pypi_publisher(self):
"""PyPI URL produces PyPI publisher."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title", url="https://pypi.org/project/requests/"
)
assert "PB - Python Package Index (PyPI)" in result
def test_generic_domain_as_publisher(self):
"""Unknown domain used as publisher name."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title", url="https://example.com/article"
)
assert "PB - example.com" in result
def test_www_stripped_from_domain(self):
"""www. prefix is stripped from domain."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title", url="https://www.example.org/page"
)
assert "PB - example.org" in result
def test_no_url_no_publisher(self):
"""No URL produces no PB or UR entries."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Title")
assert "PB -" not in result
assert "UR -" not in result
def test_empty_url_no_publisher(self):
"""Empty string URL produces no PB or UR entries."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Title", url="")
assert "PB -" not in result
assert "UR -" not in result
def test_github_subdomain(self):
"""GitHub subdomain detected as GitHub."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title", url="https://pages.github.com/project"
)
assert "PB - GitHub" in result
def test_arxiv_subdomain(self):
"""arXiv subdomain detected as arXiv."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title", url="https://export.arxiv.org/abs/1234"
)
assert "PB - arXiv" in result
class TestRISEntryTitleCleaning:
"""Tests for title cleaning patterns."""
def _make(self):
return RISExporter()
def test_doi_removed_from_title(self):
"""DOI text is removed from the title."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "My Paper DOI: 10.1234/test\nMore content"
)
# The title line should not contain DOI
ti_line = [
line for line in result.split("\n") if line.startswith("TI -")
][0]
assert "DOI:" not in ti_line
def test_published_in_removed_from_title(self):
"""'Published in ...' is removed from the title."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "My Paper Published in Nature 2023\nContent"
)
ti_line = [
line for line in result.split("\n") if line.startswith("TI -")
][0]
assert "Published in" not in ti_line
def test_volume_removed_from_title(self):
"""'Volume ...' is removed from the title."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "My Paper Volume 42 Issue 3\nContent"
)
ti_line = [
line for line in result.split("\n") if line.startswith("TI -")
][0]
assert "Volume" not in ti_line
def test_pages_removed_from_title(self):
"""'Pages ...' is removed from the title."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "My Paper Pages 100-120\nContent"
)
ti_line = [
line for line in result.split("\n") if line.startswith("TI -")
][0]
assert "Pages" not in ti_line
def test_fallback_to_original_title_when_clean_empty(self):
"""If cleaning empties the title, original title is used."""
exporter = self._make()
# Title that's entirely "by Author" where author match starts at 0
result = exporter._create_ris_entry("1", "by John Smith\nContent")
# Should still have a TI entry (either cleaned or fallback)
ti_line = [
line for line in result.split("\n") if line.startswith("TI -")
][0]
assert "TI - " in ti_line
class TestRISEntryStructure:
"""Tests for overall RIS entry structure."""
def _make(self):
return RISExporter()
def test_contains_ty_elec(self):
"""Entry contains TY - ELEC type marker."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Title")
assert "TY - ELEC" in result
def test_ends_with_er(self):
"""Entry ends with ER marker."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Title")
lines = result.split("\n")
assert lines[-1] == "ER - "
def test_contains_language(self):
"""Entry contains language tag."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Title")
assert "LA - en" in result
@freeze_time("2025-06-15")
def test_contains_access_year(self):
"""Entry contains current access year."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Title")
assert "Y1 - 2025" in result
@freeze_time("2025-06-15")
def test_contains_access_date(self):
"""Entry contains formatted access date."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Title")
assert "DA - 2025/06/15" in result
def test_ref_id_format(self):
"""Reference ID formatted as 'refN'."""
exporter = self._make()
result = exporter._create_ris_entry("42", "Title")
assert "ID - ref42" in result
class TestRISEntryYearExtraction:
"""Tests for year extraction from text."""
def _make(self):
return RISExporter()
def test_year_1990s(self):
"""Extracts 1990s year."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Published in 1995")
assert "PY - 1995" in result
def test_year_2000s(self):
"""Extracts 2000s year."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Released 2003")
assert "PY - 2003" in result
def test_no_year(self):
"""No year in text produces no PY entry."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Title with no date")
assert "PY -" not in result
def test_first_year_extracted(self):
"""First valid year in text is extracted."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Written in 2020, revised in 2023"
)
assert "PY - 2020" in result
def test_year_boundary_1900(self):
"""Year 1900 is valid (19xx pattern)."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Historical text from 1900")
assert "PY - 1900" in result
def test_non_year_four_digit_ignored(self):
"""Four-digit numbers outside 19xx-20xx range are not matched."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Article number 3456 in series"
)
assert "PY -" not in result
class TestRISEntryURLHandling:
"""Tests for URL handling in RIS entries."""
def _make(self):
return RISExporter()
def test_url_included(self):
"""URL is included as UR entry."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title", url="https://example.com"
)
assert "UR - https://example.com" in result
def test_url_with_path(self):
"""Full URL with path is preserved."""
exporter = self._make()
url = "https://example.com/path/to/article?id=123"
result = exporter._create_ris_entry("1", "Title", url=url)
assert f"UR - {url}" in result
class TestRISEntryStructuralValidity:
"""Regression guard for the accumulator-clobber bug.
``_create_ris_entry`` initialized ``lines = []`` as the RIS-output
accumulator, then immediately reassigned ``lines = full_text.split("\\n")``
to read the title — overwriting the accumulator with the raw source body.
Every RIS field was then appended *after* those source lines, so each entry
emitted the source text before the mandatory leading ``TY - `` tag and
reference managers (Zotero/Mendeley/EndNote) rejected the record. The
pre-existing tests only used ``"<tag>" in result`` substring checks, so they
never caught the leaked prefix.
"""
# RIS records are newline-separated ``XX - value`` lines where the tag is
# two uppercase-or-digit chars (TY, ID, TI, AU, DO, PY, UR, PB, Y1, DA, LA,
# ER), two spaces, a hyphen, then a space.
_RIS_LINE = re.compile(r"^[A-Z][A-Z0-9] - ")
def test_entry_starts_with_ty_tag(self):
"""RIS requires ``TY`` to be the first tag of the record."""
exporter = RISExporter()
source = (
"Understanding Widgets\n"
"URL: https://example.com/widgets\n"
"Collection: Gadgets"
)
result = exporter._create_ris_entry(
"7", source, url="https://example.com/widgets"
)
assert result.startswith("TY - "), result
def test_no_raw_source_lines_leak(self):
"""Every non-blank output line is a RIS tag, never raw source body."""
exporter = RISExporter()
source = (
"My Title\n"
"BODYLEAKMARKER this descriptive line must never be emitted\n"
"Collection: Internal"
)
result = exporter._create_ris_entry("1", source)
assert "BODYLEAKMARKER" not in result
for line in result.split("\n"):
if not line.strip():
continue
assert self._RIS_LINE.match(line), f"non-RIS line leaked: {line!r}"