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
814 lines
28 KiB
Python
814 lines
28 KiB
Python
"""
|
|
Tests for search_utilities.py — format_links_to_markdown and format_findings
|
|
edge cases not covered by the existing test_search_utilities.py and
|
|
test_search_utilities_extended.py files.
|
|
|
|
Tests cover:
|
|
- format_links_to_markdown: None/empty URL handling, special characters, index
|
|
aggregation for 'link' key fallback
|
|
- format_findings: iteration numbering, question_by_iteration edge cases,
|
|
findings with search results that have errors, malformed finding structures
|
|
"""
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# format_links_to_markdown — additional edge cases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestFormatLinksToMarkdownNoneUrls:
|
|
"""Tests for format_links_to_markdown with None/empty URLs."""
|
|
|
|
def test_none_url_skipped(self):
|
|
"""Links with None url are skipped."""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_links_to_markdown,
|
|
)
|
|
|
|
links = [
|
|
{"title": "Valid", "url": "https://valid.com", "index": "1"},
|
|
{"title": "None URL", "url": None, "index": "2"},
|
|
]
|
|
|
|
result = format_links_to_markdown(links)
|
|
|
|
assert "https://valid.com" in result
|
|
assert "None URL" not in result
|
|
|
|
def test_empty_string_url_skipped(self):
|
|
"""Links with empty string url are skipped."""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_links_to_markdown,
|
|
)
|
|
|
|
links = [
|
|
{"title": "Empty URL", "url": "", "index": "1"},
|
|
{"title": "Good", "url": "https://good.com", "index": "2"},
|
|
]
|
|
|
|
result = format_links_to_markdown(links)
|
|
|
|
assert "https://good.com" in result
|
|
assert "Empty URL" not in result
|
|
|
|
def test_no_url_or_link_key_skipped(self):
|
|
"""Links with neither 'url' nor 'link' key are skipped."""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_links_to_markdown,
|
|
)
|
|
|
|
links = [
|
|
{"title": "No URL key", "index": "1"},
|
|
]
|
|
|
|
result = format_links_to_markdown(links)
|
|
|
|
assert "No URL key" not in result
|
|
|
|
|
|
class TestFormatLinksToMarkdownSpecialChars:
|
|
"""Tests for format_links_to_markdown with special characters."""
|
|
|
|
def test_url_with_query_params(self):
|
|
"""URLs with query parameters are preserved."""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_links_to_markdown,
|
|
)
|
|
|
|
links = [
|
|
{
|
|
"title": "Search",
|
|
"url": "https://example.com/search?q=hello+world&lang=en",
|
|
"index": "1",
|
|
}
|
|
]
|
|
|
|
result = format_links_to_markdown(links)
|
|
assert "https://example.com/search?q=hello+world&lang=en" in result
|
|
|
|
def test_url_with_fragment(self):
|
|
"""URLs with fragments are stripped in the Sources display.
|
|
|
|
Fragments are anchor-only and have no effect on click-through
|
|
landing pages; the canonical form drops them for a cleaner UI.
|
|
"""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_links_to_markdown,
|
|
)
|
|
|
|
links = [
|
|
{
|
|
"title": "Section",
|
|
"url": "https://example.com/page#section-2",
|
|
"index": "1",
|
|
}
|
|
]
|
|
|
|
result = format_links_to_markdown(links)
|
|
assert "URL: https://example.com/page" in result
|
|
assert "#section-2" not in result
|
|
|
|
def test_title_with_special_markdown_chars(self):
|
|
"""Titles with markdown-special chars are included."""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_links_to_markdown,
|
|
)
|
|
|
|
links = [
|
|
{
|
|
"title": "Results [2024] — *Important*",
|
|
"url": "https://example.com",
|
|
"index": "1",
|
|
}
|
|
]
|
|
|
|
result = format_links_to_markdown(links)
|
|
assert "Results [2024]" in result
|
|
|
|
|
|
class TestFormatLinksToMarkdownIndexAggregation:
|
|
"""Tests for index aggregation in format_links_to_markdown."""
|
|
|
|
def test_link_key_fallback_aggregates_indices(self):
|
|
"""'link' key fallback still aggregates indices for same URL."""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_links_to_markdown,
|
|
)
|
|
|
|
links = [
|
|
{"title": "Same", "link": "https://same.com", "index": "1"},
|
|
{"title": "Same", "link": "https://same.com", "index": "4"},
|
|
]
|
|
|
|
result = format_links_to_markdown(links)
|
|
assert result.count("https://same.com") == 1
|
|
assert "1" in result
|
|
assert "4" in result
|
|
|
|
def test_mixed_url_and_link_keys(self):
|
|
"""Handles mix of 'url' and 'link' keys for same actual URL."""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_links_to_markdown,
|
|
)
|
|
|
|
links = [
|
|
{"title": "First", "url": "https://a.com", "index": "1"},
|
|
{"title": "Second", "link": "https://b.com", "index": "2"},
|
|
]
|
|
|
|
result = format_links_to_markdown(links)
|
|
assert "https://a.com" in result
|
|
assert "https://b.com" in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# format_findings — additional edge cases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestFormatFindingsIterationNumbering:
|
|
"""Tests for iteration numbering in format_findings."""
|
|
|
|
def test_single_iteration_with_questions(self):
|
|
"""Single iteration with questions formatted correctly."""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_findings,
|
|
)
|
|
|
|
questions = {1: ["Q1", "Q2", "Q3"]}
|
|
|
|
result = format_findings([], "Summary content", questions)
|
|
|
|
assert "Iteration 1" in result
|
|
assert "1. Q1" in result
|
|
assert "2. Q2" in result
|
|
assert "3. Q3" in result
|
|
|
|
def test_non_sequential_iteration_numbers(self):
|
|
"""Non-sequential iteration numbers (e.g., 1, 3, 5) handled correctly."""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_findings,
|
|
)
|
|
|
|
questions = {
|
|
1: ["First iter Q"],
|
|
3: ["Third iter Q"],
|
|
5: ["Fifth iter Q"],
|
|
}
|
|
|
|
result = format_findings([], "Summary", questions)
|
|
|
|
assert "Iteration 1" in result
|
|
assert "Iteration 3" in result
|
|
assert "Iteration 5" in result
|
|
assert "Iteration 2" not in result
|
|
|
|
def test_zero_iteration_key(self):
|
|
"""Iteration 0 (used by IterDRAG) is formatted."""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_findings,
|
|
)
|
|
|
|
questions = {0: ["Sub-query A", "Sub-query B"]}
|
|
|
|
result = format_findings([], "Summary", questions)
|
|
|
|
assert "Iteration 0" in result
|
|
assert "Sub-query A" in result
|
|
|
|
|
|
class TestFormatFindingsMalformedStructures:
|
|
"""Tests for malformed finding structures."""
|
|
|
|
def test_finding_missing_content_key(self):
|
|
"""Finding without 'content' key uses default."""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_findings,
|
|
)
|
|
|
|
findings = [{"phase": "Phase 1", "search_results": []}]
|
|
|
|
result = format_findings(findings, "Summary", {})
|
|
|
|
assert "No content available" in result
|
|
|
|
def test_finding_missing_phase_key(self):
|
|
"""Finding without 'phase' key uses default."""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_findings,
|
|
)
|
|
|
|
findings = [{"content": "Some content", "search_results": []}]
|
|
|
|
result = format_findings(findings, "Summary", {})
|
|
|
|
assert "Unknown Phase" in result
|
|
assert "Some content" in result
|
|
|
|
def test_finding_with_only_question_field(self):
|
|
"""Finding with only 'question' field displays it."""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_findings,
|
|
)
|
|
|
|
findings = [
|
|
{
|
|
"phase": "Custom",
|
|
"content": "Answer text",
|
|
"question": "How does X work?",
|
|
"search_results": [],
|
|
}
|
|
]
|
|
|
|
result = format_findings(findings, "Summary", {})
|
|
|
|
assert "How does X work?" in result
|
|
assert "SEARCH QUESTION" in result
|
|
|
|
def test_empty_questions_dict_shows_warning_section(self):
|
|
"""Empty questions dict (no iterations) omits section."""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_findings,
|
|
)
|
|
|
|
result = format_findings([], "Summary", {})
|
|
|
|
assert "SEARCH QUESTIONS BY ITERATION" not in result
|
|
|
|
def test_findings_with_search_results_error(self):
|
|
"""Findings with non-list search_results handled gracefully."""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_findings,
|
|
)
|
|
|
|
findings = [
|
|
{
|
|
"phase": "Search",
|
|
"content": "Content",
|
|
"search_results": None, # None instead of list
|
|
}
|
|
]
|
|
|
|
# Should not crash
|
|
result = format_findings(findings, "Summary", {})
|
|
assert "Content" in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _format_quality_tag — journal quality tags in source lists
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestFormatQualityTag:
|
|
"""Test the _format_quality_tag helper for source list quality indicators."""
|
|
|
|
def test_none_returns_empty(self):
|
|
from local_deep_research.utilities.search_utilities import (
|
|
_format_quality_tag,
|
|
)
|
|
|
|
assert _format_quality_tag(None) == ""
|
|
|
|
def test_elite_tier(self):
|
|
from local_deep_research.utilities.search_utilities import (
|
|
_format_quality_tag,
|
|
)
|
|
|
|
assert "Q1" in _format_quality_tag(10)
|
|
assert "Q1" in _format_quality_tag(9)
|
|
|
|
def test_strong_tier(self):
|
|
from local_deep_research.utilities.search_utilities import (
|
|
_format_quality_tag,
|
|
)
|
|
|
|
assert "Q1" in _format_quality_tag(7)
|
|
assert "Q1" in _format_quality_tag(8)
|
|
|
|
def test_moderate_tier(self):
|
|
from local_deep_research.utilities.search_utilities import (
|
|
_format_quality_tag,
|
|
)
|
|
|
|
assert "Q2" in _format_quality_tag(5)
|
|
assert "Q2" in _format_quality_tag(6)
|
|
|
|
def test_default_unknown_tier(self):
|
|
"""Score 4 (JOURNAL_QUALITY_DEFAULT) is rendered as [Unranked ★]."""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
_format_quality_tag,
|
|
)
|
|
|
|
assert _format_quality_tag(4) == " [Unranked ★]"
|
|
|
|
def test_predatory_tier(self):
|
|
"""Score 1 (predatory — normally auto-removed) falls back to Q4."""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
_format_quality_tag,
|
|
)
|
|
|
|
assert "Q4" in _format_quality_tag(1)
|
|
|
|
def test_score_boundary_5_is_q2_not_unranked(self):
|
|
"""Score 5 crosses the Q2 threshold; must not fall through to Unranked."""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
_format_quality_tag,
|
|
)
|
|
|
|
assert _format_quality_tag(5) == " [Q2 ★★★]"
|
|
|
|
def test_score_3_renders_unranked_not_q4(self):
|
|
"""Score 3 is the filter's 'no scoring data' fallback — rendered as
|
|
Unranked rather than Q4 so it reads as "we don't know this venue"
|
|
rather than "we know it's low-quality"."""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
_format_quality_tag,
|
|
)
|
|
|
|
assert _format_quality_tag(3) == " [Unranked ★]"
|
|
|
|
def test_preprint_sentinel(self):
|
|
"""QUALITY_PREPRINT marks results with no journal_ref at all."""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
QUALITY_PREPRINT,
|
|
_format_quality_tag,
|
|
)
|
|
|
|
tag = _format_quality_tag(QUALITY_PREPRINT)
|
|
assert "preprint" in tag
|
|
assert "not in journal catalog" in tag
|
|
|
|
def test_pending_sentinel(self):
|
|
from local_deep_research.utilities.search_utilities import (
|
|
QUALITY_PENDING,
|
|
_format_quality_tag,
|
|
)
|
|
|
|
assert "downloading" in _format_quality_tag(QUALITY_PENDING)
|
|
|
|
def test_out_of_range_value_surfaces_raw(self):
|
|
"""Out-of-set inputs must not silently bucket into Q4 — the
|
|
catch-all renders ``[quality=<value>]`` so bad scoring logic is
|
|
visible in the output.
|
|
"""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
_format_quality_tag,
|
|
)
|
|
|
|
tag = _format_quality_tag(99)
|
|
assert "quality=" in tag
|
|
assert "99" in tag
|
|
|
|
def test_every_valid_score_has_explicit_branch(self):
|
|
"""Every value in VALID_QUALITY_SCORES must map to a real tier
|
|
tag — none should fall through to the debug catch-all.
|
|
"""
|
|
from local_deep_research.constants import VALID_QUALITY_SCORES
|
|
from local_deep_research.utilities.search_utilities import (
|
|
_format_quality_tag,
|
|
)
|
|
|
|
for score in VALID_QUALITY_SCORES:
|
|
tag = _format_quality_tag(score)
|
|
assert "quality=" not in tag, (
|
|
f"score {score} fell through to the debug catch-all: {tag!r}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# format_links_to_markdown — dedup via canonical URL key
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestFormatLinksDedupCanonical:
|
|
"""Canonicalized dedup: slight URL variants collapse to one Sources entry."""
|
|
|
|
def setup_method(self):
|
|
# canonical_url_key is cached across tests; clear so each test starts
|
|
# from a clean slate (otherwise earlier test inputs can pollute the
|
|
# expected-output behavior if we ever depend on cache-miss timing).
|
|
from local_deep_research.utilities.url_utils import canonical_url_key
|
|
|
|
canonical_url_key.cache_clear()
|
|
|
|
def test_dedup_trailing_slash(self):
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_links_to_markdown,
|
|
)
|
|
|
|
links = [
|
|
{"title": "Page", "url": "https://x.com/p", "index": "1"},
|
|
{"title": "Page", "url": "https://x.com/p/", "index": "2"},
|
|
]
|
|
result = format_links_to_markdown(links)
|
|
# One entry, two indices.
|
|
assert result.count("URL: https://x.com/p") == 1
|
|
assert "[1, 2]" in result
|
|
|
|
def test_dedup_tracking_params(self):
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_links_to_markdown,
|
|
)
|
|
|
|
links = [
|
|
{
|
|
"title": "Vox",
|
|
"url": "https://www.vox.com/a?utm_source=g&utm_medium=s",
|
|
"index": "1",
|
|
},
|
|
{"title": "Vox", "url": "https://www.vox.com/a", "index": "2"},
|
|
]
|
|
result = format_links_to_markdown(links)
|
|
assert "[1, 2]" in result
|
|
# Canonical (clean) URL is displayed — tracking params stripped.
|
|
assert "utm_source" not in result
|
|
assert "utm_medium" not in result
|
|
assert "URL: https://www.vox.com/a" in result
|
|
|
|
def test_dedup_fragment_variants(self):
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_links_to_markdown,
|
|
)
|
|
|
|
links = [
|
|
{"title": "Wiki", "url": "https://x.com/p", "index": "1"},
|
|
{
|
|
"title": "Wiki",
|
|
"url": "https://x.com/p#section",
|
|
"index": "2",
|
|
},
|
|
]
|
|
result = format_links_to_markdown(links)
|
|
assert "[1, 2]" in result
|
|
|
|
def test_dedup_default_port(self):
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_links_to_markdown,
|
|
)
|
|
|
|
links = [
|
|
{"title": "Site", "url": "https://x.com/p", "index": "1"},
|
|
{"title": "Site", "url": "https://x.com:443/p", "index": "2"},
|
|
]
|
|
result = format_links_to_markdown(links)
|
|
assert "[1, 2]" in result
|
|
|
|
def test_distinct_urls_stay_distinct(self):
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_links_to_markdown,
|
|
)
|
|
|
|
links = [
|
|
{"title": "A", "url": "https://x.com/a", "index": "1"},
|
|
{"title": "B", "url": "https://x.com/b", "index": "2"},
|
|
]
|
|
result = format_links_to_markdown(links)
|
|
assert "https://x.com/a" in result
|
|
assert "https://x.com/b" in result
|
|
assert "[1, 2]" not in result
|
|
|
|
def test_display_shows_canonical_url_not_raw(self):
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_links_to_markdown,
|
|
)
|
|
|
|
# Even when the utm-bearing variant comes first, the Sources
|
|
# section shows the canonical (tracking-stripped) URL — cleaner
|
|
# UI, identical click-through behavior.
|
|
links = [
|
|
{
|
|
"title": "Article",
|
|
"url": "https://x.com/a?utm_source=g",
|
|
"index": "1",
|
|
},
|
|
{"title": "Article", "url": "https://x.com/a", "index": "2"},
|
|
]
|
|
result = format_links_to_markdown(links)
|
|
assert "URL: https://x.com/a\n" in result
|
|
assert "utm_source" not in result
|
|
|
|
def test_display_strips_userinfo(self):
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_links_to_markdown,
|
|
)
|
|
|
|
# Userinfo in a URL would leak credentials if displayed. Canonical
|
|
# form drops it, so the Sources section is always safe.
|
|
links = [
|
|
{
|
|
"title": "Internal",
|
|
"url": "https://user:secret@internal.example.com/p",
|
|
"index": "1",
|
|
}
|
|
]
|
|
result = format_links_to_markdown(links)
|
|
assert "secret" not in result
|
|
assert "user:" not in result
|
|
assert "URL: https://internal.example.com/p" in result
|
|
|
|
def test_ref_param_not_stripped(self):
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_links_to_markdown,
|
|
)
|
|
|
|
# ?ref= on GitHub is a branch selector, not a tracker.
|
|
links = [
|
|
{
|
|
"title": "Repo on main",
|
|
"url": "https://github.com/o/r?ref=main",
|
|
"index": "1",
|
|
},
|
|
{
|
|
"title": "Repo on dev",
|
|
"url": "https://github.com/o/r?ref=dev",
|
|
"index": "2",
|
|
},
|
|
]
|
|
result = format_links_to_markdown(links)
|
|
# Must remain two separate entries.
|
|
assert "ref=main" in result
|
|
assert "ref=dev" in result
|
|
assert "[1, 2]" not in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# format_findings — exact character-by-character output
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestFormatFindingsExactOutput:
|
|
"""Character-by-character tests of the markdown produced by
|
|
``format_findings`` for predetermined inputs. Locks in the exact
|
|
layout (indentation, blank lines, ``[1]`` citation markers, the
|
|
80-char ``_`` separator) so a renderer regression — even a single
|
|
extra newline or a moved citation — fails loudly.
|
|
"""
|
|
|
|
def test_exact_output_minimal_findings(self):
|
|
"""One finding, one source, one iteration, one question."""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_findings,
|
|
)
|
|
|
|
findings_list = [
|
|
{
|
|
"phase": "Initial",
|
|
"content": "Qubits enable quantum computing [1].",
|
|
"search_results": [
|
|
{
|
|
"title": "Qubit Basics",
|
|
"link": "https://example.com/q",
|
|
"index": "1",
|
|
},
|
|
],
|
|
},
|
|
]
|
|
synthesized_content = "Quantum computing uses qubits [1]."
|
|
questions_by_iteration = {1: ["What is a qubit?"]}
|
|
|
|
# Expected output is the verbatim string format_findings produces
|
|
# for the inputs above. Anchored to today's exact layout — any
|
|
# change (added blank line, moved citation, renamed section
|
|
# header) will fail this test, which is the point.
|
|
expected = (
|
|
"Quantum computing uses qubits [1].\n"
|
|
"\n"
|
|
"[1] Qubit Basics (source nr: 1)\n"
|
|
" URL: https://example.com/q\n"
|
|
"\n"
|
|
"\n"
|
|
"\n"
|
|
"\n"
|
|
"## SEARCH QUESTIONS BY ITERATION\n"
|
|
"\n"
|
|
"\n"
|
|
" #### Iteration 1:\n"
|
|
"1. What is a qubit?\n"
|
|
"\n"
|
|
"\n"
|
|
"\n"
|
|
"## DETAILED FINDINGS\n"
|
|
"\n"
|
|
"\n"
|
|
"### Initial\n"
|
|
"\n"
|
|
"\n"
|
|
"\n"
|
|
"\n"
|
|
"Qubits enable quantum computing [1].\n"
|
|
"\n"
|
|
"### SOURCES USED IN THIS SECTION:\n"
|
|
"[1] Qubit Basics (source nr: 1)\n"
|
|
" URL: https://example.com/q\n"
|
|
"\n"
|
|
"\n"
|
|
"\n"
|
|
"\n" + ("_" * 80) + "\n"
|
|
"\n"
|
|
"## ALL SOURCES:\n"
|
|
"[1] Qubit Basics (source nr: 1)\n"
|
|
" URL: https://example.com/q\n"
|
|
"\n"
|
|
"\n"
|
|
)
|
|
|
|
actual = format_findings(
|
|
findings_list, synthesized_content, questions_by_iteration
|
|
)
|
|
assert actual == expected, (
|
|
f"format_findings output drifted from the locked-in layout.\n"
|
|
f"--- expected ---\n{expected!r}\n"
|
|
f"--- actual ---\n{actual!r}"
|
|
)
|
|
|
|
def test_citations_in_synthesized_content_preserved(self):
|
|
"""``[1]`` and ``[2]`` markers in the synthesized content reach
|
|
the final output unchanged. Independent narrower assertion to
|
|
catch citation-handling regressions even when the broader exact-
|
|
match test is in flux for unrelated layout reasons."""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_findings,
|
|
)
|
|
|
|
synthesized = "First fact [1]. Second fact [2]. Third fact [1]."
|
|
result = format_findings(
|
|
findings_list=[],
|
|
synthesized_content=synthesized,
|
|
questions_by_iteration={},
|
|
)
|
|
# The synthesized content is the very first thing written.
|
|
assert result.startswith(synthesized + "\n\n"), (
|
|
f"Synthesized content (with citations) was rewritten or "
|
|
f"prefixed by format_findings: result starts with "
|
|
f"{result[: len(synthesized) + 20]!r}"
|
|
)
|
|
assert result.count("[1]") == 2
|
|
assert result.count("[2]") == 1
|
|
|
|
def test_source_indices_match_citation_markers(self):
|
|
"""When a finding's search_results have indices ``"1"`` and
|
|
``"2"``, format_links_to_markdown emits ``[1]`` and ``[2]`` —
|
|
these should align with the citation markers in the synthesized
|
|
content. Tests the citation→source alignment that makes the
|
|
markdown navigable."""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_findings,
|
|
)
|
|
|
|
findings_list = [
|
|
{
|
|
"phase": "Initial",
|
|
"content": "Body referencing [1] and [2].",
|
|
"search_results": [
|
|
{
|
|
"title": "Source One",
|
|
"link": "https://example.com/one",
|
|
"index": "1",
|
|
},
|
|
{
|
|
"title": "Source Two",
|
|
"link": "https://example.com/two",
|
|
"index": "2",
|
|
},
|
|
],
|
|
},
|
|
]
|
|
result = format_findings(
|
|
findings_list,
|
|
synthesized_content="See [1] and [2].",
|
|
questions_by_iteration={},
|
|
)
|
|
# Both [1] and [2] appear in source listings, attached to the
|
|
# right titles and URLs.
|
|
assert "[1] Source One" in result
|
|
assert "https://example.com/one" in result
|
|
assert "[2] Source Two" in result
|
|
assert "https://example.com/two" in result
|
|
# And the synthesized [1]/[2] citations at the top survive.
|
|
assert result.startswith("See [1] and [2].\n\n")
|
|
|
|
|
|
class TestFormatLinksToMarkdownCollections:
|
|
"""Tests for the optional ``Collection:`` line surfaced for
|
|
RAG / library results so the source-tagged citation mode can read
|
|
the collection name back from the rendered sources block."""
|
|
|
|
def test_emits_collection_line_when_metadata_present(self):
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_links_to_markdown,
|
|
)
|
|
|
|
links = [
|
|
{
|
|
"title": "Local Paper",
|
|
"url": "/library/document/abc123",
|
|
"index": "1",
|
|
"metadata": {"collection_name": "My Papers"},
|
|
},
|
|
]
|
|
result = format_links_to_markdown(links)
|
|
assert "URL: /library/document/abc123" in result
|
|
assert "Collection: My Papers" in result
|
|
|
|
def test_no_collection_line_when_metadata_absent(self):
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_links_to_markdown,
|
|
)
|
|
|
|
# Plain web hit, no metadata.
|
|
links = [
|
|
{
|
|
"title": "Web Page",
|
|
"url": "https://example.com/page",
|
|
"index": "1",
|
|
},
|
|
]
|
|
result = format_links_to_markdown(links)
|
|
assert "URL: https://example.com/page" in result
|
|
assert "Collection:" not in result
|
|
|
|
def test_no_collection_line_when_metadata_lacks_collection_name(self):
|
|
"""metadata may exist for other reasons (engine_name, score, etc.)
|
|
without carrying a collection name. Don't emit the line then."""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_links_to_markdown,
|
|
)
|
|
|
|
links = [
|
|
{
|
|
"title": "Web Page",
|
|
"url": "https://example.com/page",
|
|
"index": "1",
|
|
"metadata": {"engine_name": "web", "score": 0.91},
|
|
},
|
|
]
|
|
result = format_links_to_markdown(links)
|
|
assert "Collection:" not in result
|
|
|
|
def test_first_non_empty_collection_wins_per_url(self):
|
|
"""Two hits for the same canonical URL — the first source with a
|
|
non-empty collection name sets it; later hits don't overwrite.
|
|
Mirrors how title and journal_quality work."""
|
|
from local_deep_research.utilities.search_utilities import (
|
|
format_links_to_markdown,
|
|
)
|
|
|
|
links = [
|
|
{
|
|
"title": "Doc",
|
|
"url": "/library/document/abc",
|
|
"index": "1",
|
|
"metadata": {"collection_name": "first"},
|
|
},
|
|
{
|
|
"title": "Doc",
|
|
"url": "/library/document/abc",
|
|
"index": "2",
|
|
"metadata": {"collection_name": "second"},
|
|
},
|
|
]
|
|
result = format_links_to_markdown(links)
|
|
# Single entry (URLs deduped), carries the first collection.
|
|
assert "Collection: first" in result
|
|
assert "Collection: second" not in result
|