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
285 lines
9.8 KiB
Python
285 lines
9.8 KiB
Python
"""
|
|
Citation regression tests covering:
|
|
- Parametrized test across all 5 CitationModes
|
|
- Multi-source DOMAIN_ID_HYPERLINKS with repeated domains
|
|
- Export round-trip (Quarto BibTeX consistency)
|
|
- LaTeX math-mode edge cases
|
|
- RIS author extraction false positives
|
|
- Quarto hardcoded year=2024
|
|
|
|
Source: src/local_deep_research/text_optimization/citation_formatter.py
|
|
"""
|
|
|
|
import re
|
|
|
|
import pytest
|
|
|
|
from local_deep_research.text_optimization.citation_formatter import (
|
|
CitationFormatter,
|
|
CitationMode,
|
|
QuartoExporter,
|
|
RISExporter,
|
|
LaTeXExporter,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Shared test content
|
|
# ---------------------------------------------------------------------------
|
|
|
|
SAMPLE_CONTENT = """# Research Report
|
|
|
|
This is about AI [1] and ML [2].
|
|
|
|
## Sources
|
|
|
|
[1] Introduction to Artificial Intelligence
|
|
URL: https://arxiv.org/abs/2024.12345
|
|
|
|
[2] Machine Learning Fundamentals
|
|
URL: https://github.com/ml-project/fundamentals
|
|
"""
|
|
|
|
MULTI_DOMAIN_CONTENT = """# Report
|
|
|
|
Topic A [1] and topic B [2] with extra [3].
|
|
|
|
## Sources
|
|
|
|
[1] First arxiv paper
|
|
URL: https://arxiv.org/abs/2024.001
|
|
|
|
[2] Second arxiv paper
|
|
URL: https://arxiv.org/abs/2024.002
|
|
|
|
[3] GitHub project
|
|
URL: https://github.com/project/repo
|
|
"""
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Parametrized test across all 5 modes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestAllCitationModes:
|
|
"""Same document formatted across all 5 CitationModes."""
|
|
|
|
@pytest.mark.parametrize(
|
|
"mode,expected_fragment",
|
|
[
|
|
(CitationMode.NUMBER_HYPERLINKS, "[[1]](https://arxiv.org"),
|
|
(CitationMode.DOMAIN_HYPERLINKS, "[[arxiv.org]](https://arxiv.org"),
|
|
(CitationMode.DOMAIN_ID_HYPERLINKS, "arxiv.org"),
|
|
(CitationMode.DOMAIN_ID_ALWAYS_HYPERLINKS, "arxiv.org-1"),
|
|
(CitationMode.NO_HYPERLINKS, "[1]"),
|
|
],
|
|
)
|
|
def test_mode_produces_expected_fragment(self, mode, expected_fragment):
|
|
"""Each mode produces its characteristic citation fragment."""
|
|
formatter = CitationFormatter(mode)
|
|
result = formatter.format_document(SAMPLE_CONTENT)
|
|
assert expected_fragment in result
|
|
|
|
def test_no_hyperlinks_returns_content_unchanged(self):
|
|
"""NO_HYPERLINKS mode returns content unchanged."""
|
|
formatter = CitationFormatter(CitationMode.NO_HYPERLINKS)
|
|
result = formatter.format_document(SAMPLE_CONTENT)
|
|
assert result == SAMPLE_CONTENT
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Multi-source DOMAIN_ID_HYPERLINKS
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestDomainIdMultiSource:
|
|
"""DOMAIN_ID_HYPERLINKS adds IDs only for repeated domains."""
|
|
|
|
def test_repeated_domain_gets_ids(self):
|
|
"""Two arxiv sources get arxiv.org-1 and arxiv.org-2."""
|
|
formatter = CitationFormatter(CitationMode.DOMAIN_ID_HYPERLINKS)
|
|
result = formatter.format_document(MULTI_DOMAIN_CONTENT)
|
|
assert "arxiv.org-1" in result or "arxiv.org-2" in result
|
|
|
|
def test_unique_domain_no_id(self):
|
|
"""Single github source gets just github.com (no -1)."""
|
|
formatter = CitationFormatter(CitationMode.DOMAIN_ID_HYPERLINKS)
|
|
result = formatter.format_document(MULTI_DOMAIN_CONTENT)
|
|
# github.com should appear without -1 suffix
|
|
assert "[[github.com]]" in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Quarto export
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestQuartoExport:
|
|
"""Quarto export round-trip and consistency."""
|
|
|
|
def test_all_refs_have_bibtex_entries(self):
|
|
"""Every @refN in Quarto body has matching BibTeX @misc{refN,...}."""
|
|
exporter = QuartoExporter()
|
|
result = exporter.export_to_quarto(SAMPLE_CONTENT, "Test Report")
|
|
|
|
# Find all @refN references in body
|
|
ref_pattern = re.compile(r"@ref(\d+)")
|
|
refs_in_body = set(ref_pattern.findall(result))
|
|
|
|
# Find all BibTeX entries
|
|
bib_pattern = re.compile(r"@misc\{ref(\d+),")
|
|
bib_entries = set(bib_pattern.findall(result))
|
|
|
|
assert refs_in_body, "Should have at least one @ref in body"
|
|
assert refs_in_body.issubset(bib_entries), (
|
|
f"Missing BibTeX entries for: {refs_in_body - bib_entries}"
|
|
)
|
|
|
|
def test_bibtex_keys_unique(self):
|
|
"""All BibTeX keys are unique."""
|
|
exporter = QuartoExporter()
|
|
result = exporter.export_to_quarto(SAMPLE_CONTENT)
|
|
|
|
bib_pattern = re.compile(r"@misc\{(ref\d+),")
|
|
keys = bib_pattern.findall(result)
|
|
assert len(keys) == len(set(keys)), f"Duplicate BibTeX keys: {keys}"
|
|
|
|
def test_hardcoded_year_2024(self):
|
|
"""Documents the hardcoded year = {2024} at line 543."""
|
|
exporter = QuartoExporter()
|
|
result = exporter.export_to_quarto(SAMPLE_CONTENT)
|
|
|
|
# Every BibTeX entry should have year = {2024}
|
|
assert "year = {2024}" in result
|
|
|
|
def test_yaml_header_included(self):
|
|
"""Quarto output includes YAML front matter."""
|
|
exporter = QuartoExporter()
|
|
result = exporter.export_to_quarto(SAMPLE_CONTENT, "My Report")
|
|
assert result.startswith("---")
|
|
assert 'title: "My Report"' in result
|
|
|
|
def test_comma_citations_converted(self):
|
|
"""Comma-separated citations [1, 2] converted to [@ref1, @ref2]."""
|
|
content = "Text [1, 2] here.\n\n## Sources\n\n[1] A\nURL: https://a.com\n\n[2] B\nURL: https://b.com\n"
|
|
exporter = QuartoExporter()
|
|
result = exporter.export_to_quarto(content)
|
|
assert "@ref1" in result
|
|
assert "@ref2" in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# RIS export
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestRISExport:
|
|
"""RIS format export tests."""
|
|
|
|
def test_basic_ris_structure(self):
|
|
"""RIS output has TY, TI, UR, ER tags."""
|
|
exporter = RISExporter()
|
|
result = exporter.export_to_ris(SAMPLE_CONTENT)
|
|
assert "TY - ELEC" in result
|
|
assert "TI -" in result
|
|
assert "ER - " in result
|
|
|
|
def test_url_included_in_ris(self):
|
|
"""URL from source is included in RIS entry."""
|
|
exporter = RISExporter()
|
|
result = exporter.export_to_ris(SAMPLE_CONTENT)
|
|
assert "UR - https://arxiv.org" in result
|
|
|
|
def test_author_extraction_by_keyword(self):
|
|
"""'by Author Name' extracts author correctly."""
|
|
content = """# Report
|
|
Text [1].
|
|
## Sources
|
|
[1] Research Paper by John Smith and Jane Doe
|
|
URL: https://example.com
|
|
"""
|
|
exporter = RISExporter()
|
|
result = exporter.export_to_ris(content)
|
|
assert "AU -" in result
|
|
|
|
def test_author_false_positive_stand_by_me(self):
|
|
"""'Stand by Me' should NOT extract 'Me' as an author.
|
|
|
|
The regex \\bby\\s+([^.\\n]+?) matches 'by Me' in 'Stand by Me'.
|
|
This documents the false positive behavior of the current regex.
|
|
"""
|
|
content = """# Report
|
|
Text [1].
|
|
## Sources
|
|
[1] Stand by Me - A Classic Film Analysis
|
|
URL: https://example.com
|
|
"""
|
|
exporter = RISExporter()
|
|
result = exporter.export_to_ris(content)
|
|
# The regex WILL match "by Me" — this documents the current behavior
|
|
# The "AU - Me" line documents this false positive
|
|
if "AU -" in result:
|
|
# If authors are extracted, "Me" would be one of them
|
|
# This is a known false positive from the simple regex
|
|
pass # Document behavior without asserting it's correct
|
|
|
|
def test_no_sources_section_returns_empty(self):
|
|
"""Content without sources section returns empty string."""
|
|
exporter = RISExporter()
|
|
result = exporter.export_to_ris("Just some text without sources")
|
|
assert result == ""
|
|
|
|
def test_arxiv_publisher(self):
|
|
"""arxiv.org URL produces PB - arXiv."""
|
|
exporter = RISExporter()
|
|
result = exporter.export_to_ris(SAMPLE_CONTENT)
|
|
assert "PB - arXiv" in result
|
|
|
|
def test_github_publisher(self):
|
|
"""github.com URL produces PB - GitHub."""
|
|
exporter = RISExporter()
|
|
result = exporter.export_to_ris(SAMPLE_CONTENT)
|
|
assert "PB - GitHub" in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# LaTeX export
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestLaTeXExport:
|
|
"""LaTeX export edge cases."""
|
|
|
|
def test_basic_latex_structure(self):
|
|
"""LaTeX output includes documentclass and begin{document}."""
|
|
exporter = LaTeXExporter()
|
|
result = exporter.export_to_latex(SAMPLE_CONTENT)
|
|
assert "\\documentclass" in result
|
|
assert "\\begin{document}" in result
|
|
assert "\\end{document}" in result
|
|
|
|
def test_headings_converted(self):
|
|
"""Markdown headings converted to LaTeX sections."""
|
|
exporter = LaTeXExporter()
|
|
result = exporter.export_to_latex("# Title\n\n## Subtitle\n\nText")
|
|
assert "\\section{" in result
|
|
assert "\\subsection{" in result
|
|
|
|
def test_unbalanced_dollar_sign(self):
|
|
"""Unbalanced $ (e.g., 'costs $100') in content.
|
|
|
|
LaTeX exporter should handle or at least not crash on unbalanced $.
|
|
"""
|
|
content = "# Report\n\nThis costs $100 per unit.\n\n## Sources\n\n[1] Test\nURL: https://a.com\n"
|
|
exporter = LaTeXExporter()
|
|
# Should not raise
|
|
result = exporter.export_to_latex(content)
|
|
assert isinstance(result, str)
|
|
|
|
def test_citations_converted_to_cite(self):
|
|
"""[1] converted to \\cite{ref1} or similar."""
|
|
exporter = LaTeXExporter()
|
|
result = exporter.export_to_latex(SAMPLE_CONTENT)
|
|
assert "\\cite{" in result or "ref1" in result
|