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
1490 lines
53 KiB
Python
1490 lines
53 KiB
Python
"""
|
|
Behavioral tests for citation_formatter module.
|
|
|
|
Tests CitationFormatter, QuartoExporter, RISExporter, and LaTeXExporter classes.
|
|
"""
|
|
|
|
|
|
class TestCitationModeEnum:
|
|
"""Tests for CitationMode enum."""
|
|
|
|
def test_number_hyperlinks_mode_exists(self):
|
|
"""NUMBER_HYPERLINKS mode exists."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationMode,
|
|
)
|
|
|
|
assert hasattr(CitationMode, "NUMBER_HYPERLINKS")
|
|
|
|
def test_domain_hyperlinks_mode_exists(self):
|
|
"""DOMAIN_HYPERLINKS mode exists."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationMode,
|
|
)
|
|
|
|
assert hasattr(CitationMode, "DOMAIN_HYPERLINKS")
|
|
|
|
def test_domain_id_hyperlinks_mode_exists(self):
|
|
"""DOMAIN_ID_HYPERLINKS mode exists."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationMode,
|
|
)
|
|
|
|
assert hasattr(CitationMode, "DOMAIN_ID_HYPERLINKS")
|
|
|
|
def test_domain_id_always_hyperlinks_mode_exists(self):
|
|
"""DOMAIN_ID_ALWAYS_HYPERLINKS mode exists."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationMode,
|
|
)
|
|
|
|
assert hasattr(CitationMode, "DOMAIN_ID_ALWAYS_HYPERLINKS")
|
|
|
|
def test_no_hyperlinks_mode_exists(self):
|
|
"""NO_HYPERLINKS mode exists."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationMode,
|
|
)
|
|
|
|
assert hasattr(CitationMode, "NO_HYPERLINKS")
|
|
|
|
def test_mode_values_are_strings(self):
|
|
"""All mode values are strings."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationMode,
|
|
)
|
|
|
|
for mode in CitationMode:
|
|
assert isinstance(mode.value, str)
|
|
|
|
|
|
class TestCitationFormatterInit:
|
|
"""Tests for CitationFormatter initialization."""
|
|
|
|
def test_default_mode_is_number_hyperlinks(self):
|
|
"""Default mode is NUMBER_HYPERLINKS."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
CitationMode,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
assert formatter.mode == CitationMode.NUMBER_HYPERLINKS
|
|
|
|
def test_accepts_custom_mode(self):
|
|
"""Accepts custom citation mode."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
CitationMode,
|
|
)
|
|
|
|
formatter = CitationFormatter(mode=CitationMode.DOMAIN_HYPERLINKS)
|
|
assert formatter.mode == CitationMode.DOMAIN_HYPERLINKS
|
|
|
|
def test_has_citation_pattern(self):
|
|
"""Has compiled citation pattern regex."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
assert formatter.citation_pattern is not None
|
|
|
|
def test_has_comma_citation_pattern(self):
|
|
"""Has compiled comma citation pattern regex."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
assert formatter.comma_citation_pattern is not None
|
|
|
|
def test_has_source_word_pattern(self):
|
|
"""Has compiled source word pattern regex."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
assert formatter.source_word_pattern is not None
|
|
|
|
|
|
class TestCitationFormatterExtractDomain:
|
|
"""Tests for _extract_domain method."""
|
|
|
|
def test_extracts_simple_domain(self):
|
|
"""Extracts simple domain from URL."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
result = formatter._extract_domain("https://example.com/page")
|
|
assert result == "example.com"
|
|
|
|
def test_removes_www_prefix(self):
|
|
"""Removes www. prefix from domain."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
result = formatter._extract_domain("https://www.example.com/page")
|
|
assert result == "example.com"
|
|
|
|
def test_recognizes_known_domains(self):
|
|
"""Recognizes and preserves known domains."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
known_urls = [
|
|
("https://arxiv.org/abs/1234", "arxiv.org"),
|
|
("https://github.com/user/repo", "github.com"),
|
|
("https://reddit.com/r/test", "reddit.com"),
|
|
("https://youtube.com/watch", "youtube.com"),
|
|
("https://pypi.org/project/test", "pypi.org"),
|
|
("https://milvus.io/docs", "milvus.io"),
|
|
("https://medium.com/article", "medium.com"),
|
|
]
|
|
for url, expected in known_urls:
|
|
result = formatter._extract_domain(url)
|
|
assert result == expected, f"Failed for {url}"
|
|
|
|
def test_handles_subdomain(self):
|
|
"""Handles subdomain in URL."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
result = formatter._extract_domain("https://docs.python.org/page")
|
|
assert result == "python.org"
|
|
|
|
def test_handles_invalid_url(self):
|
|
"""Returns 'source' for invalid URL."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
result = formatter._extract_domain("not-a-valid-url")
|
|
# Should return something sensible, not crash
|
|
assert isinstance(result, str)
|
|
|
|
def test_handles_empty_url(self):
|
|
"""Handles empty URL string."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
result = formatter._extract_domain("")
|
|
assert isinstance(result, str)
|
|
|
|
|
|
class TestCitationFormatterFindSourcesSection:
|
|
"""Tests for _find_sources_section method."""
|
|
|
|
def test_finds_sources_header(self):
|
|
"""Finds ## Sources header."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "Some content\n\n## Sources\n\n[1] Source one"
|
|
result = formatter._find_sources_section(content)
|
|
assert result != -1
|
|
assert content[result:].startswith("## Sources")
|
|
|
|
def test_finds_references_header(self):
|
|
"""Finds ## References header."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "Some content\n\n## References\n\n[1] Ref one"
|
|
result = formatter._find_sources_section(content)
|
|
assert result != -1
|
|
|
|
def test_finds_bibliography_header(self):
|
|
"""Finds ## Bibliography header."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "Some content\n\n## Bibliography\n\n[1] Bib one"
|
|
result = formatter._find_sources_section(content)
|
|
assert result != -1
|
|
|
|
def test_finds_citations_header(self):
|
|
"""Finds ## Citations header."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "Some content\n\n## Citations\n\n[1] Citation one"
|
|
result = formatter._find_sources_section(content)
|
|
assert result != -1
|
|
|
|
def test_case_insensitive(self):
|
|
"""Finds sources header case-insensitively."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "Some content\n\n## SOURCES\n\n[1] Source one"
|
|
result = formatter._find_sources_section(content)
|
|
assert result != -1
|
|
|
|
def test_returns_minus_one_when_not_found(self):
|
|
"""Returns -1 when no sources section found."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "Some content without sources section."
|
|
result = formatter._find_sources_section(content)
|
|
assert result == -1
|
|
|
|
def test_finds_single_hash_header(self):
|
|
"""Finds # Sources header."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "Some content\n\n# Sources\n\n[1] Source one"
|
|
result = formatter._find_sources_section(content)
|
|
assert result != -1
|
|
|
|
def test_finds_triple_hash_header(self):
|
|
"""Finds ### Sources header."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "Some content\n\n### Sources\n\n[1] Source one"
|
|
result = formatter._find_sources_section(content)
|
|
assert result != -1
|
|
|
|
|
|
class TestCitationFormatterParseSources:
|
|
"""Tests for _parse_sources method."""
|
|
|
|
def test_parses_single_source(self):
|
|
"""Parses a single source entry."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
sources_content = "[1] Test Source Title\n URL: https://example.com"
|
|
result = formatter._parse_sources(sources_content)
|
|
assert "1" in result
|
|
assert result["1"][0] == "Test Source Title"
|
|
assert result["1"][1] == "https://example.com"
|
|
|
|
def test_parses_multiple_sources(self):
|
|
"""Parses multiple source entries."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
sources_content = """[1] First Source
|
|
URL: https://first.com
|
|
[2] Second Source
|
|
URL: https://second.com"""
|
|
result = formatter._parse_sources(sources_content)
|
|
assert "1" in result
|
|
assert "2" in result
|
|
|
|
def test_parses_source_without_url(self):
|
|
"""Parses source without URL."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
sources_content = "[1] Source Without URL"
|
|
result = formatter._parse_sources(sources_content)
|
|
assert "1" in result
|
|
assert result["1"][1] == "" # Empty URL
|
|
|
|
def test_parses_comma_separated_citation_numbers(self):
|
|
"""Parses comma-separated citation numbers like [36, 3]."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
sources_content = (
|
|
"[36, 3] Shared Source Title\n URL: https://shared.com"
|
|
)
|
|
result = formatter._parse_sources(sources_content)
|
|
assert "36" in result
|
|
assert "3" in result
|
|
assert result["36"][0] == "Shared Source Title"
|
|
assert result["3"][0] == "Shared Source Title"
|
|
|
|
def test_returns_empty_dict_for_empty_content(self):
|
|
"""Returns empty dict for empty content."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
result = formatter._parse_sources("")
|
|
assert result == {}
|
|
|
|
|
|
class TestCitationFormatterFormatDocument:
|
|
"""Tests for format_document method."""
|
|
|
|
def test_returns_unchanged_for_no_hyperlinks_mode(self):
|
|
"""Returns unchanged content in NO_HYPERLINKS mode."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
CitationMode,
|
|
)
|
|
|
|
formatter = CitationFormatter(mode=CitationMode.NO_HYPERLINKS)
|
|
content = "Text with [1] citation.\n\n## Sources\n\n[1] Source\n URL: https://test.com"
|
|
result = formatter.format_document(content)
|
|
assert result == content
|
|
|
|
def test_returns_unchanged_when_no_sources_section(self):
|
|
"""Returns unchanged when no sources section found."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "Text with [1] citation but no sources section."
|
|
result = formatter.format_document(content)
|
|
assert result == content
|
|
|
|
def test_formats_number_hyperlinks(self):
|
|
"""Formats citations with number hyperlinks."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
CitationMode,
|
|
)
|
|
|
|
formatter = CitationFormatter(mode=CitationMode.NUMBER_HYPERLINKS)
|
|
content = "See [1] for details.\n\n## Sources\n\n[1] Test Source\n URL: https://test.com"
|
|
result = formatter.format_document(content)
|
|
assert "[[1]](https://test.com)" in result
|
|
|
|
def test_formats_domain_hyperlinks(self):
|
|
"""Formats citations with domain hyperlinks."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
CitationMode,
|
|
)
|
|
|
|
formatter = CitationFormatter(mode=CitationMode.DOMAIN_HYPERLINKS)
|
|
content = "See [1] for details.\n\n## Sources\n\n[1] Test Source\n URL: https://example.com/page"
|
|
result = formatter.format_document(content)
|
|
assert "example.com" in result
|
|
|
|
def test_preserves_sources_section(self):
|
|
"""Preserves the sources section unchanged."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
sources_section = (
|
|
"## Sources\n\n[1] Test Source\n URL: https://test.com"
|
|
)
|
|
content = f"See [1] here.\n\n{sources_section}"
|
|
result = formatter.format_document(content)
|
|
assert sources_section in result
|
|
|
|
|
|
class TestCitationFormatterFormatNumberHyperlinks:
|
|
"""Tests for _format_number_hyperlinks method."""
|
|
|
|
def test_formats_single_citation(self):
|
|
"""Formats a single citation with number hyperlink."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "Text with [1] here."
|
|
sources = {"1": ("Title", "https://example.com")}
|
|
result = formatter._format_number_hyperlinks(content, sources)
|
|
assert "[[1]](https://example.com)" in result
|
|
|
|
def test_formats_comma_separated_citations(self):
|
|
"""Formats comma-separated citations."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "Text with [1, 2] here."
|
|
sources = {
|
|
"1": ("Title 1", "https://one.com"),
|
|
"2": ("Title 2", "https://two.com"),
|
|
}
|
|
result = formatter._format_number_hyperlinks(content, sources)
|
|
assert "[[1]](https://one.com)" in result
|
|
assert "[[2]](https://two.com)" in result
|
|
|
|
def test_formats_source_word_pattern(self):
|
|
"""Formats 'Source X' patterns."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "According to Source 1, this is true."
|
|
sources = {"1": ("Title", "https://example.com")}
|
|
result = formatter._format_number_hyperlinks(content, sources)
|
|
assert "[[1]](https://example.com)" in result
|
|
|
|
def test_leaves_citation_without_url_unchanged(self):
|
|
"""Leaves citation without URL unchanged."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "Text with [1] here."
|
|
sources = {"1": ("Title", "")} # No URL
|
|
result = formatter._format_number_hyperlinks(content, sources)
|
|
assert "[1]" in result
|
|
# Should not have hyperlink
|
|
assert "[[1]](" not in result
|
|
|
|
|
|
class TestCitationFormatterFormatDomainHyperlinks:
|
|
"""Tests for _format_domain_hyperlinks method."""
|
|
|
|
def test_replaces_number_with_domain(self):
|
|
"""Replaces citation number with domain."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "Text with [1] here."
|
|
sources = {"1": ("Title", "https://github.com/repo")}
|
|
result = formatter._format_domain_hyperlinks(content, sources)
|
|
assert "github.com" in result
|
|
|
|
def test_handles_comma_separated_citations(self):
|
|
"""Handles comma-separated citations with domains."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "Text with [1, 2] here."
|
|
sources = {
|
|
"1": ("Title 1", "https://github.com"),
|
|
"2": ("Title 2", "https://arxiv.org"),
|
|
}
|
|
result = formatter._format_domain_hyperlinks(content, sources)
|
|
assert "github.com" in result
|
|
assert "arxiv.org" in result
|
|
|
|
|
|
class TestCitationFormatterFormatDomainIdHyperlinks:
|
|
"""Tests for _format_domain_id_hyperlinks method."""
|
|
|
|
def test_no_id_for_single_domain_citation(self):
|
|
"""No ID suffix when only one citation from domain."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "Text with [1] here."
|
|
sources = {"1": ("Title", "https://github.com/repo")}
|
|
result = formatter._format_domain_id_hyperlinks(content, sources)
|
|
assert "[[github.com]](" in result
|
|
assert "-1" not in result.split("github.com")[1].split("]")[0]
|
|
|
|
def test_adds_id_for_multiple_domain_citations(self):
|
|
"""Adds ID suffix when multiple citations from same domain."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "Text with [1] and [2] here."
|
|
sources = {
|
|
"1": ("Title 1", "https://github.com/repo1"),
|
|
"2": ("Title 2", "https://github.com/repo2"),
|
|
}
|
|
result = formatter._format_domain_id_hyperlinks(content, sources)
|
|
assert "github.com-1" in result
|
|
assert "github.com-2" in result
|
|
|
|
|
|
class TestCitationFormatterFormatDomainIdAlwaysHyperlinks:
|
|
"""Tests for _format_domain_id_always_hyperlinks method."""
|
|
|
|
def test_always_adds_id(self):
|
|
"""Always adds ID suffix even for single citation."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "Text with [1] here."
|
|
sources = {"1": ("Title", "https://github.com/repo")}
|
|
result = formatter._format_domain_id_always_hyperlinks(content, sources)
|
|
assert "github.com-1" in result
|
|
|
|
|
|
class TestCitationFormatterUnicodeBrackets:
|
|
"""Tests for handling Unicode lenticular brackets."""
|
|
|
|
def test_matches_unicode_lenticular_brackets(self):
|
|
"""Matches Unicode lenticular brackets 【】."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "Text with 【1】 here."
|
|
sources = {"1": ("Title", "https://example.com")}
|
|
result = formatter._format_number_hyperlinks(content, sources)
|
|
assert "[[1]](https://example.com)" in result
|
|
|
|
|
|
class TestQuartoExporterInit:
|
|
"""Tests for QuartoExporter initialization."""
|
|
|
|
def test_can_instantiate(self):
|
|
"""Can instantiate QuartoExporter."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
QuartoExporter,
|
|
)
|
|
|
|
exporter = QuartoExporter()
|
|
assert exporter is not None
|
|
|
|
def test_has_citation_pattern(self):
|
|
"""Has citation pattern regex."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
QuartoExporter,
|
|
)
|
|
|
|
exporter = QuartoExporter()
|
|
assert exporter.citation_pattern is not None
|
|
|
|
def test_has_comma_citation_pattern(self):
|
|
"""Has comma citation pattern regex."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
QuartoExporter,
|
|
)
|
|
|
|
exporter = QuartoExporter()
|
|
assert exporter.comma_citation_pattern is not None
|
|
|
|
|
|
class TestQuartoExporterExportToQuarto:
|
|
"""Tests for export_to_quarto method."""
|
|
|
|
def test_adds_yaml_header(self):
|
|
"""Adds YAML front matter header."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
QuartoExporter,
|
|
)
|
|
|
|
exporter = QuartoExporter()
|
|
content = "# My Report\n\nSome content."
|
|
result = exporter.export_to_quarto(content)
|
|
assert result.startswith("---")
|
|
assert "title:" in result
|
|
|
|
def test_uses_provided_title(self):
|
|
"""Uses provided title in YAML header."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
QuartoExporter,
|
|
)
|
|
|
|
exporter = QuartoExporter()
|
|
content = "Some content."
|
|
result = exporter.export_to_quarto(content, title="Custom Title")
|
|
assert "Custom Title" in result
|
|
|
|
def test_extracts_title_from_content(self):
|
|
"""Extracts title from content if not provided."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
QuartoExporter,
|
|
)
|
|
|
|
exporter = QuartoExporter()
|
|
content = "# My Research Report\n\nSome content."
|
|
result = exporter.export_to_quarto(content)
|
|
assert "My Research Report" in result
|
|
|
|
def test_converts_citations_to_quarto_format(self):
|
|
"""Converts [1] citations to [@ref1] format."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
QuartoExporter,
|
|
)
|
|
|
|
exporter = QuartoExporter()
|
|
content = "See [1] for details."
|
|
result = exporter.export_to_quarto(content)
|
|
assert "[@ref1]" in result
|
|
|
|
def test_converts_comma_separated_citations(self):
|
|
"""Converts comma-separated citations to Quarto format."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
QuartoExporter,
|
|
)
|
|
|
|
exporter = QuartoExporter()
|
|
content = "See [1, 2, 3] for details."
|
|
result = exporter.export_to_quarto(content)
|
|
assert "@ref1" in result
|
|
assert "@ref2" in result
|
|
assert "@ref3" in result
|
|
|
|
def test_adds_bibliography_note(self):
|
|
"""Adds bibliography file note."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
QuartoExporter,
|
|
)
|
|
|
|
exporter = QuartoExporter()
|
|
content = (
|
|
"Content.\n\n## Sources\n\n[1] Source One\n URL: https://test.com"
|
|
)
|
|
result = exporter.export_to_quarto(content)
|
|
assert "references.bib" in result
|
|
|
|
|
|
class TestQuartoExporterGenerateBibliography:
|
|
"""Tests for _generate_bibliography method."""
|
|
|
|
def test_generates_bibtex_entries(self):
|
|
"""Generates BibTeX entries from sources."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
QuartoExporter,
|
|
)
|
|
|
|
exporter = QuartoExporter()
|
|
content = "[1] Test Source Title\n URL: https://example.com"
|
|
result = exporter._generate_bibliography(content)
|
|
assert "@misc{ref1" in result
|
|
# BibTeX uses braces and quotes: title = "{Title}"
|
|
assert "Test Source Title" in result
|
|
assert "title" in result
|
|
|
|
def test_includes_url_in_bibtex(self):
|
|
"""Includes URL in BibTeX entry."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
QuartoExporter,
|
|
)
|
|
|
|
exporter = QuartoExporter()
|
|
content = "[1] Test Source Title\n URL: https://example.com"
|
|
result = exporter._generate_bibliography(content)
|
|
assert "url = {https://example.com}" in result
|
|
|
|
def test_generates_multiple_entries(self):
|
|
"""Generates multiple BibTeX entries."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
QuartoExporter,
|
|
)
|
|
|
|
exporter = QuartoExporter()
|
|
content = "[1] First Source\n URL: https://first.com\n[2] Second Source\n URL: https://second.com"
|
|
result = exporter._generate_bibliography(content)
|
|
assert "@misc{ref1" in result
|
|
assert "@misc{ref2" in result
|
|
|
|
|
|
class TestRISExporterInit:
|
|
"""Tests for RISExporter initialization."""
|
|
|
|
def test_can_instantiate(self):
|
|
"""Can instantiate RISExporter."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
RISExporter,
|
|
)
|
|
|
|
exporter = RISExporter()
|
|
assert exporter is not None
|
|
|
|
def test_has_sources_pattern(self):
|
|
"""Has sources pattern regex."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
RISExporter,
|
|
)
|
|
|
|
exporter = RISExporter()
|
|
assert exporter.sources_pattern is not None
|
|
|
|
|
|
class TestRISExporterExportToRIS:
|
|
"""Tests for export_to_ris method."""
|
|
|
|
def test_returns_empty_for_no_sources(self):
|
|
"""Returns empty string when no sources section."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
RISExporter,
|
|
)
|
|
|
|
exporter = RISExporter()
|
|
content = "Content without sources section."
|
|
result = exporter.export_to_ris(content)
|
|
assert result == ""
|
|
|
|
def test_finds_sources_section(self):
|
|
"""Finds ## Sources section."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
RISExporter,
|
|
)
|
|
|
|
exporter = RISExporter()
|
|
content = "Content.\n\n## Sources\n\n[1] Test Source\n URL: https://test.com"
|
|
result = exporter.export_to_ris(content)
|
|
assert "TY - ELEC" in result
|
|
|
|
def test_finds_references_section(self):
|
|
"""Finds ## References section."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
RISExporter,
|
|
)
|
|
|
|
exporter = RISExporter()
|
|
content = "Content.\n\n## References\n\n[1] Test Ref\n URL: https://test.com"
|
|
result = exporter.export_to_ris(content)
|
|
assert "TY - ELEC" in result
|
|
|
|
def test_includes_title(self):
|
|
"""Includes title in RIS entry."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
RISExporter,
|
|
)
|
|
|
|
exporter = RISExporter()
|
|
content = "## Sources\n\n[1] My Test Title"
|
|
result = exporter.export_to_ris(content)
|
|
assert "TI -" in result
|
|
|
|
def test_includes_url(self):
|
|
"""Includes URL in RIS entry."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
RISExporter,
|
|
)
|
|
|
|
exporter = RISExporter()
|
|
content = "## Sources\n\n[1] Test Source\nURL: https://example.com"
|
|
result = exporter.export_to_ris(content)
|
|
assert "UR - https://example.com" in result
|
|
|
|
def test_includes_end_of_reference(self):
|
|
"""Includes ER - end of reference marker."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
RISExporter,
|
|
)
|
|
|
|
exporter = RISExporter()
|
|
content = "## Sources\n\n[1] Test Source"
|
|
result = exporter.export_to_ris(content)
|
|
assert "ER -" in result
|
|
|
|
def test_includes_language(self):
|
|
"""Includes language field in RIS."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
RISExporter,
|
|
)
|
|
|
|
exporter = RISExporter()
|
|
content = "## Sources\n\n[1] Test Source"
|
|
result = exporter.export_to_ris(content)
|
|
assert "LA - en" in result
|
|
|
|
|
|
class TestRISExporterCreateRISEntry:
|
|
"""Tests for _create_ris_entry method."""
|
|
|
|
def test_creates_entry_with_id(self):
|
|
"""Creates RIS entry with reference ID."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
RISExporter,
|
|
)
|
|
|
|
exporter = RISExporter()
|
|
result = exporter._create_ris_entry("1", "Test Title")
|
|
assert "ID - ref1" in result
|
|
|
|
def test_extracts_year_from_text(self):
|
|
"""Extracts year from text if present."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
RISExporter,
|
|
)
|
|
|
|
exporter = RISExporter()
|
|
result = exporter._create_ris_entry("1", "Test Title published in 2023")
|
|
assert "PY - 2023" in result
|
|
|
|
def test_extracts_doi_from_metadata(self):
|
|
"""Extracts DOI from metadata."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
RISExporter,
|
|
)
|
|
|
|
exporter = RISExporter()
|
|
result = exporter._create_ris_entry(
|
|
"1", "Test Title", metadata={"doi": "10.1234/test"}
|
|
)
|
|
assert "DO - 10.1234/test" in result
|
|
|
|
def test_extracts_publisher_from_github_url(self):
|
|
"""Extracts GitHub as publisher from URL."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
RISExporter,
|
|
)
|
|
|
|
exporter = RISExporter()
|
|
result = exporter._create_ris_entry(
|
|
"1", "Test Title", url="https://github.com/user/repo"
|
|
)
|
|
assert "PB - GitHub" in result
|
|
|
|
def test_extracts_publisher_from_arxiv_url(self):
|
|
"""Extracts arXiv as publisher from URL."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
RISExporter,
|
|
)
|
|
|
|
exporter = RISExporter()
|
|
result = exporter._create_ris_entry(
|
|
"1", "Test Title", url="https://arxiv.org/abs/1234"
|
|
)
|
|
assert "PB - arXiv" in result
|
|
|
|
|
|
class TestLaTeXExporterInit:
|
|
"""Tests for LaTeXExporter initialization."""
|
|
|
|
def test_can_instantiate(self):
|
|
"""Can instantiate LaTeXExporter."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
assert exporter is not None
|
|
|
|
def test_has_citation_pattern(self):
|
|
"""Has citation pattern regex."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
assert exporter.citation_pattern is not None
|
|
|
|
def test_has_heading_patterns(self):
|
|
"""Has heading patterns list."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
assert len(exporter.heading_patterns) > 0
|
|
|
|
def test_has_emphasis_patterns(self):
|
|
"""Has emphasis patterns list."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
assert len(exporter.emphasis_patterns) > 0
|
|
|
|
|
|
class TestLaTeXExporterExportToLatex:
|
|
"""Tests for export_to_latex method."""
|
|
|
|
def test_adds_document_class(self):
|
|
"""Adds documentclass declaration."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
result = exporter.export_to_latex("Content")
|
|
assert "\\documentclass" in result
|
|
|
|
def test_adds_begin_document(self):
|
|
"""Adds begin{document}."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
result = exporter.export_to_latex("Content")
|
|
assert "\\begin{document}" in result
|
|
|
|
def test_adds_end_document(self):
|
|
"""Adds end{document}."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
result = exporter.export_to_latex("Content")
|
|
assert "\\end{document}" in result
|
|
|
|
def test_converts_h1_to_section(self):
|
|
"""Converts # heading to \\section."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
result = exporter.export_to_latex("# My Heading\n\nContent")
|
|
assert "\\section{My Heading}" in result
|
|
|
|
def test_converts_h2_to_subsection(self):
|
|
"""Converts ## heading to \\subsection."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
result = exporter.export_to_latex("## My Subheading\n\nContent")
|
|
assert "\\subsection{My Subheading}" in result
|
|
|
|
def test_converts_h3_to_subsubsection(self):
|
|
"""Converts ### heading to \\subsubsection."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
result = exporter.export_to_latex("### My Sub-Subheading\n\nContent")
|
|
assert "\\subsubsection{My Sub-Subheading}" in result
|
|
|
|
def test_converts_bold_to_textbf(self):
|
|
"""Converts **bold** to \\textbf."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
result = exporter.export_to_latex("This is **bold** text.")
|
|
assert "\\textbf{bold}" in result
|
|
|
|
def test_converts_italic_to_textit(self):
|
|
"""Converts *italic* to \\textit."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
result = exporter.export_to_latex("This is *italic* text.")
|
|
assert "\\textit{italic}" in result
|
|
|
|
def test_converts_code_to_texttt(self):
|
|
"""Converts `code` to \\texttt."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
result = exporter.export_to_latex("This is `code` text.")
|
|
assert "\\texttt{code}" in result
|
|
|
|
def test_converts_citations_to_cite(self):
|
|
"""Converts [1] to \\cite{1}."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
result = exporter.export_to_latex("See [1] for details.")
|
|
assert "\\cite{1}" in result
|
|
|
|
def test_adds_bibliography(self):
|
|
"""Adds bibliography section when sources exist."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
content = "Content.\n\n## Sources\n\n[1] Test Source\n URL: https://test.com"
|
|
result = exporter.export_to_latex(content)
|
|
assert "\\begin{thebibliography}" in result
|
|
assert "\\end{thebibliography}" in result
|
|
|
|
|
|
class TestLaTeXExporterConvertLists:
|
|
"""Tests for _convert_lists method."""
|
|
|
|
def test_converts_bullet_to_item(self):
|
|
"""Converts - bullet to \\item."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
result = exporter._convert_lists("- First item\n- Second item")
|
|
assert "\\item First item" in result
|
|
assert "\\item Second item" in result
|
|
|
|
def test_adds_itemize_environment(self):
|
|
"""Adds itemize environment around list."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
result = exporter._convert_lists("- First item\n- Second item")
|
|
assert "\\begin{itemize}" in result
|
|
assert "\\end{itemize}" in result
|
|
|
|
|
|
class TestLaTeXExporterEscapeLatex:
|
|
"""Tests for _escape_latex method."""
|
|
|
|
def test_escapes_ampersand(self):
|
|
"""Escapes & character."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
result = exporter._escape_latex("A & B")
|
|
assert "\\&" in result
|
|
|
|
def test_escapes_percent(self):
|
|
"""Escapes % character."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
result = exporter._escape_latex("50%")
|
|
assert "\\%" in result
|
|
|
|
def test_escapes_dollar(self):
|
|
"""Escapes $ character."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
result = exporter._escape_latex("$100")
|
|
assert "\\$" in result
|
|
|
|
def test_escapes_hash(self):
|
|
"""Escapes # character."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
result = exporter._escape_latex("#1")
|
|
assert "\\#" in result
|
|
|
|
def test_escapes_underscore(self):
|
|
"""Escapes _ character."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
result = exporter._escape_latex("file_name")
|
|
assert "\\_" in result
|
|
|
|
def test_escapes_braces(self):
|
|
"""Escapes { and } characters."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
result = exporter._escape_latex("{test}")
|
|
assert "\\{" in result
|
|
assert "\\}" in result
|
|
|
|
def test_escapes_tilde(self):
|
|
"""Escapes ~ character."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
result = exporter._escape_latex("~test")
|
|
assert "\\textasciitilde{}" in result
|
|
|
|
def test_escapes_caret(self):
|
|
"""Escapes ^ character."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
result = exporter._escape_latex("x^2")
|
|
assert "\\textasciicircum{}" in result
|
|
|
|
|
|
class TestLaTeXExporterCreateBibliography:
|
|
"""Tests for _create_bibliography method."""
|
|
|
|
def test_returns_empty_for_no_sources(self):
|
|
"""Returns empty string when no sources section."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
result = exporter._create_bibliography("Content without sources.")
|
|
assert result == ""
|
|
|
|
def test_creates_bibitem_entries(self):
|
|
"""Creates bibitem entries for sources."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
content = "## Sources\n\n[1] Test Source Title"
|
|
result = exporter._create_bibliography(content)
|
|
assert "\\bibitem{1}" in result
|
|
|
|
def test_includes_url_in_bibitem(self):
|
|
"""Includes URL in bibitem entry."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
LaTeXExporter,
|
|
)
|
|
|
|
exporter = LaTeXExporter()
|
|
content = (
|
|
"## Sources\n\n[1] Test Source Title\n URL: https://example.com"
|
|
)
|
|
result = exporter._create_bibliography(content)
|
|
assert "\\url{https://example.com}" in result
|
|
|
|
|
|
class TestCitationFormatterFormatDocumentSplit:
|
|
"""Tests for format_document_split — answer / sources tuple return."""
|
|
|
|
def test_returns_tuple_with_answer_and_sources(self):
|
|
"""Happy path: returns hyperlinked answer and raw sources block."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
CitationMode,
|
|
)
|
|
|
|
formatter = CitationFormatter(mode=CitationMode.NUMBER_HYPERLINKS)
|
|
content = (
|
|
"See [1] for details.\n\n"
|
|
"## Sources\n\n[1] Test Source\n URL: https://test.com"
|
|
)
|
|
answer, sources = formatter.format_document_split(content)
|
|
assert "[[1]](https://test.com)" in answer
|
|
assert sources.startswith("## Sources")
|
|
assert "Test Source" in sources
|
|
|
|
def test_returns_empty_sources_when_no_sources_section(self):
|
|
"""No Sources section in input → returns (content, '')."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
CitationMode,
|
|
)
|
|
|
|
formatter = CitationFormatter(mode=CitationMode.NUMBER_HYPERLINKS)
|
|
content = "Just an answer with [1] but no Sources section."
|
|
answer, sources = formatter.format_document_split(content)
|
|
assert answer == content
|
|
assert sources == ""
|
|
|
|
def test_returns_unchanged_in_no_hyperlinks_mode(self):
|
|
"""NO_HYPERLINKS mode short-circuits to (content, '')."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
CitationMode,
|
|
)
|
|
|
|
formatter = CitationFormatter(mode=CitationMode.NO_HYPERLINKS)
|
|
content = (
|
|
"Text with [1].\n\n## Sources\n\n[1] Source\n URL: https://x.com"
|
|
)
|
|
answer, sources = formatter.format_document_split(content)
|
|
assert answer == content
|
|
assert sources == ""
|
|
|
|
def test_format_document_compat_equals_concatenation(self):
|
|
"""format_document() == answer + sources from format_document_split."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
CitationMode,
|
|
)
|
|
|
|
formatter = CitationFormatter(mode=CitationMode.NUMBER_HYPERLINKS)
|
|
content = (
|
|
"Body with [1] cite.\n\n"
|
|
"## Sources\n\n[1] One\n URL: https://one.com"
|
|
)
|
|
full = formatter.format_document(content)
|
|
answer, sources = formatter.format_document_split(content)
|
|
assert full == answer + sources
|
|
|
|
|
|
class TestCitationFormatterApplyInlineHyperlinks:
|
|
"""Tests for apply_inline_hyperlinks — structured-source delegation."""
|
|
|
|
def test_replaces_bracket_refs_with_markdown_links(self):
|
|
"""[1] [2] become [[1]](url) [[2]](url) given a structured list."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "See [1] and [2]."
|
|
sources = [
|
|
{"index": "1", "title": "First", "url": "https://a.com"},
|
|
{"index": "2", "title": "Second", "url": "https://b.com"},
|
|
]
|
|
result = formatter.apply_inline_hyperlinks(content, sources)
|
|
assert "[[1]](https://a.com)" in result
|
|
assert "[[2]](https://b.com)" in result
|
|
|
|
def test_handles_missing_index_gracefully(self):
|
|
"""[7] left unchanged when no source with index 7."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "See [1] and [7]."
|
|
sources = [
|
|
{"index": "1", "title": "Only", "url": "https://a.com"},
|
|
]
|
|
result = formatter.apply_inline_hyperlinks(content, sources)
|
|
assert "[[1]](https://a.com)" in result
|
|
# [7] should remain as-is — formatter passes through unknown indices
|
|
assert "[7]" in result
|
|
|
|
def test_handles_comma_lists(self):
|
|
"""[1, 2] becomes individual links via comma_citation_pattern."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "Per [1, 2] this is true."
|
|
sources = [
|
|
{"index": "1", "title": "First", "url": "https://a.com"},
|
|
{"index": "2", "title": "Second", "url": "https://b.com"},
|
|
]
|
|
result = formatter.apply_inline_hyperlinks(content, sources)
|
|
# _format_number_hyperlinks expands comma lists into individual links
|
|
assert "https://a.com" in result
|
|
assert "https://b.com" in result
|
|
|
|
def test_skips_when_no_bracket_refs(self):
|
|
"""Content without [N] refs returns unchanged."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "Plain text with no citations."
|
|
sources = [
|
|
{"index": "1", "title": "Unused", "url": "https://x.com"},
|
|
]
|
|
result = formatter.apply_inline_hyperlinks(content, sources)
|
|
assert result == content
|
|
|
|
def test_returns_content_unchanged_when_sources_empty(self):
|
|
"""Empty sources list short-circuits to content."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "See [1]."
|
|
assert formatter.apply_inline_hyperlinks(content, []) == content
|
|
|
|
def test_returns_empty_string_when_content_empty(self):
|
|
"""Empty content returns empty string, no crash."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
sources = [{"index": "1", "title": "x", "url": "https://x.com"}]
|
|
assert formatter.apply_inline_hyperlinks("", sources) == ""
|
|
assert formatter.apply_inline_hyperlinks(None, sources) == ""
|
|
|
|
def test_skips_sources_with_missing_url(self):
|
|
"""Sources missing url are filtered out before delegation."""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "See [1] and [2]."
|
|
sources = [
|
|
{"index": "1", "title": "Has URL", "url": "https://a.com"},
|
|
{"index": "2", "title": "No URL"}, # missing url key
|
|
]
|
|
result = formatter.apply_inline_hyperlinks(content, sources)
|
|
assert "[[1]](https://a.com)" in result
|
|
# [2] left unchanged because that source was filtered
|
|
assert "[2]" in result
|
|
|
|
def test_accepts_link_key_for_searxng_compatibility(self):
|
|
"""Regression: Searxng-sourced results use 'link' not 'url'.
|
|
|
|
search_engine_searxng.py emits ``{"link": url, "title": ...,
|
|
"snippet": ...}``. The langgraph-agent strategy's collector stores
|
|
these dicts verbatim in ``all_links_of_system`` and the chat-mode
|
|
fallback path (``apply_inline_hyperlinks``) consumed them. Looking
|
|
up only ``s["url"]`` silently dropped every Searxng-sourced
|
|
citation — the answer body shipped with plain ``[N]`` brackets
|
|
even though the Sources section beneath was fully populated.
|
|
Both keys must be accepted.
|
|
"""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "Per [1] and [2], yes."
|
|
sources = [
|
|
# Searxng shape
|
|
{"index": "1", "title": "Searxng result", "link": "https://sx.com"},
|
|
# Other-engine shape (still works)
|
|
{"index": "2", "title": "Other engine", "url": "https://o.com"},
|
|
]
|
|
result = formatter.apply_inline_hyperlinks(content, sources)
|
|
assert "[[1]](https://sx.com)" in result, (
|
|
"Searxng-style 'link' key must produce a hyperlink"
|
|
)
|
|
assert "[[2]](https://o.com)" in result
|
|
|
|
def test_prefers_url_over_link_when_both_present(self):
|
|
"""When a source carries both keys, ``url`` wins.
|
|
|
|
The langgraph collector copies the raw engine dict and additionally
|
|
sets ``r['link'] = r['url']`` when only ``url`` is present; other
|
|
callers may set both for legacy reasons. The canonical destination
|
|
is ``url`` — fall back to ``link`` only when ``url`` is missing.
|
|
"""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
)
|
|
|
|
formatter = CitationFormatter()
|
|
content = "See [1]."
|
|
sources = [
|
|
{
|
|
"index": "1",
|
|
"title": "Both",
|
|
"url": "https://canonical.com",
|
|
"link": "https://legacy.com",
|
|
}
|
|
]
|
|
result = formatter.apply_inline_hyperlinks(content, sources)
|
|
assert "[[1]](https://canonical.com)" in result
|
|
|
|
def test_dispatches_on_self_mode_for_domain_hyperlinks(self):
|
|
"""Regression: ``apply_inline_hyperlinks`` was hard-coded to
|
|
the NUMBER_HYPERLINKS formatter, so the chat-mode fallback
|
|
path ignored ``report.citation_format``. A user who picked
|
|
DOMAIN_HYPERLINKS in settings still got ``[[1]](url)`` on
|
|
every chat answer instead of ``[[arxiv.org]](url)``.
|
|
"""
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
CitationMode,
|
|
)
|
|
|
|
formatter = CitationFormatter(mode=CitationMode.DOMAIN_HYPERLINKS)
|
|
content = "Per [1] and [2]."
|
|
sources = [
|
|
{"index": "1", "title": "T", "url": "https://arxiv.org/abs/123"},
|
|
{"index": "2", "title": "T", "url": "https://nytimes.com/a"},
|
|
]
|
|
result = formatter.apply_inline_hyperlinks(content, sources)
|
|
assert "[[arxiv.org]](https://arxiv.org/abs/123)" in result
|
|
assert "[[nytimes.com]](https://nytimes.com/a)" in result
|
|
assert "[[1]](" not in result # must NOT fall back to NUMBER mode
|
|
|
|
def test_dispatches_on_self_mode_for_domain_id_hyperlinks(self):
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
CitationMode,
|
|
)
|
|
|
|
formatter = CitationFormatter(mode=CitationMode.DOMAIN_ID_HYPERLINKS)
|
|
content = "See [1] and [2]."
|
|
sources = [
|
|
{"index": "1", "title": "T", "url": "https://arxiv.org/abs/1"},
|
|
{"index": "2", "title": "T", "url": "https://arxiv.org/abs/2"},
|
|
]
|
|
result = formatter.apply_inline_hyperlinks(content, sources)
|
|
# Smart domain numbering: same domain twice → [arxiv.org-1] /
|
|
# [arxiv.org-2] (or similar — the exact suffix is the formatter's
|
|
# business; we assert it does NOT collapse to plain [N] mode).
|
|
assert "arxiv.org" in result
|
|
assert "[[1]](" not in result
|
|
|
|
def test_dispatches_on_self_mode_for_source_tagged_hyperlinks(self):
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
CitationMode,
|
|
)
|
|
|
|
formatter = CitationFormatter(
|
|
mode=CitationMode.SOURCE_TAGGED_HYPERLINKS
|
|
)
|
|
content = "Per [1] and [2]."
|
|
sources = [
|
|
{"index": "1", "title": "T", "url": "https://arxiv.org/abs/1"},
|
|
# Library hit: collection_name in metadata should win over
|
|
# the URL-derived domain tag.
|
|
{
|
|
"index": "2",
|
|
"title": "T",
|
|
"url": "https://example.com/doc",
|
|
"metadata": {"collection_name": "mypapers"},
|
|
},
|
|
]
|
|
result = formatter.apply_inline_hyperlinks(content, sources)
|
|
# arxiv URL → [arxiv-1] (URLClassifier tag), collection → [mypapers-2].
|
|
assert "arxiv-1" in result
|
|
assert "mypapers-2" in result
|
|
|
|
def test_no_hyperlinks_mode_returns_content_unchanged(self):
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
CitationMode,
|
|
)
|
|
|
|
formatter = CitationFormatter(mode=CitationMode.NO_HYPERLINKS)
|
|
content = "Per [1] and [2]."
|
|
sources = [
|
|
{"index": "1", "title": "T", "url": "https://a.com"},
|
|
{"index": "2", "title": "T", "url": "https://b.com"},
|
|
]
|
|
assert formatter.apply_inline_hyperlinks(content, sources) == content
|