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
367 lines
13 KiB
Python
367 lines
13 KiB
Python
"""
|
|
Integration tests for download_content tool in MCP strategy.
|
|
"""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from local_deep_research.content_fetcher import ContentFetcher
|
|
from local_deep_research.research_library.downloaders.base import DownloadResult
|
|
|
|
|
|
class TestMCPDownloadContentTool:
|
|
"""Test download_content tool integration with MCP strategy."""
|
|
|
|
@patch(
|
|
"local_deep_research.content_fetcher.fetcher.ContentFetcher._get_downloader"
|
|
)
|
|
def test_execute_download_content_success(self, mock_get_downloader):
|
|
"""Test successful content download via MCP strategy executor."""
|
|
# Setup mock
|
|
mock_downloader = MagicMock()
|
|
mock_downloader.download_with_result.return_value = DownloadResult(
|
|
content=b"# Research Paper\n\nThis is the abstract of the paper discussing important findings.",
|
|
is_success=True,
|
|
)
|
|
mock_downloader.get_metadata.return_value = {
|
|
"title": "Important Research Paper",
|
|
"author": "Dr. Smith",
|
|
}
|
|
mock_get_downloader.return_value = mock_downloader
|
|
|
|
# Import and test the executor pattern
|
|
from local_deep_research.content_fetcher import ContentFetcher
|
|
|
|
fetcher = ContentFetcher()
|
|
result = fetcher.fetch(
|
|
"https://arxiv.org/abs/2301.12345",
|
|
max_length=5000,
|
|
)
|
|
|
|
assert result["status"] == "success"
|
|
assert "abstract" in result["content"].lower()
|
|
assert result["source_type"] == "arXiv"
|
|
|
|
@patch(
|
|
"local_deep_research.content_fetcher.fetcher.ContentFetcher._get_downloader"
|
|
)
|
|
def test_execute_download_content_with_truncation(
|
|
self, mock_get_downloader
|
|
):
|
|
"""Test content truncation for very long papers."""
|
|
# Create a very long paper content
|
|
long_content = "Introduction. " * 5000 # ~70k characters
|
|
|
|
mock_downloader = MagicMock()
|
|
mock_downloader.download_with_result.return_value = DownloadResult(
|
|
content=long_content.encode("utf-8"),
|
|
is_success=True,
|
|
)
|
|
mock_downloader.get_metadata.return_value = {}
|
|
mock_get_downloader.return_value = mock_downloader
|
|
|
|
fetcher = ContentFetcher()
|
|
result = fetcher.fetch(
|
|
"https://arxiv.org/abs/2301.12345",
|
|
max_length=1000,
|
|
)
|
|
|
|
assert result["status"] == "success"
|
|
assert len(result["content"]) < 2000 # Should be truncated
|
|
assert "truncated" in result["content"].lower()
|
|
|
|
@patch(
|
|
"local_deep_research.content_fetcher.fetcher.ContentFetcher._get_downloader"
|
|
)
|
|
def test_execute_download_content_paper_not_found(
|
|
self, mock_get_downloader
|
|
):
|
|
"""Test handling when paper is not available."""
|
|
mock_downloader = MagicMock()
|
|
mock_downloader.download_with_result.return_value = DownloadResult(
|
|
content=None,
|
|
is_success=False,
|
|
skip_reason="Paper not found or requires subscription",
|
|
)
|
|
mock_get_downloader.return_value = mock_downloader
|
|
|
|
fetcher = ContentFetcher()
|
|
result = fetcher.fetch("https://example.com/paywalled-paper")
|
|
|
|
assert result["status"] == "error"
|
|
assert (
|
|
"subscription" in result["error"].lower()
|
|
or "not found" in result["error"].lower()
|
|
)
|
|
|
|
@patch(
|
|
"local_deep_research.content_fetcher.fetcher.ContentFetcher._get_downloader"
|
|
)
|
|
@patch(
|
|
"local_deep_research.content_fetcher.fetcher.policy_aware_validate_url",
|
|
return_value=True,
|
|
)
|
|
def test_execute_download_content_timeout(
|
|
self, mock_validate, mock_get_downloader
|
|
):
|
|
"""Test handling of timeout during download."""
|
|
mock_downloader = MagicMock()
|
|
mock_downloader.download_with_result.side_effect = TimeoutError(
|
|
"Connection timed out"
|
|
)
|
|
mock_get_downloader.return_value = mock_downloader
|
|
|
|
fetcher = ContentFetcher()
|
|
result = fetcher.fetch("https://slow-server.example.com/paper")
|
|
|
|
assert result["status"] == "error"
|
|
assert (
|
|
"timed out" in result["error"].lower()
|
|
or "timeout" in result["error"].lower()
|
|
)
|
|
|
|
|
|
class TestDownloadContentWithRealURLPatterns:
|
|
"""Test with real-world URL patterns."""
|
|
|
|
def test_arxiv_url_variants(self):
|
|
"""Test various arXiv URL formats are recognized."""
|
|
fetcher = ContentFetcher()
|
|
|
|
# Standard abstract
|
|
info = fetcher.get_url_info("https://arxiv.org/abs/2301.12345")
|
|
assert info["url_type"] == "arxiv"
|
|
assert info["extracted_id"] == "2301.12345"
|
|
|
|
# With version
|
|
info = fetcher.get_url_info("https://arxiv.org/abs/2301.12345v3")
|
|
assert info["url_type"] == "arxiv"
|
|
assert info["extracted_id"] == "2301.12345v3"
|
|
|
|
# PDF link
|
|
info = fetcher.get_url_info("https://arxiv.org/pdf/2301.12345.pdf")
|
|
assert info["url_type"] == "arxiv"
|
|
|
|
# HTML version
|
|
info = fetcher.get_url_info("https://arxiv.org/html/2301.12345")
|
|
assert info["url_type"] == "arxiv"
|
|
|
|
# ar5iv (HTML rendering)
|
|
info = fetcher.get_url_info("https://ar5iv.org/abs/2301.12345")
|
|
assert info["url_type"] == "arxiv"
|
|
|
|
# Old format (pre-2007)
|
|
info = fetcher.get_url_info("https://arxiv.org/abs/hep-th/9901001")
|
|
assert info["url_type"] == "arxiv"
|
|
|
|
def test_pubmed_url_variants(self):
|
|
"""Test various PubMed URL formats."""
|
|
fetcher = ContentFetcher()
|
|
|
|
# Standard PubMed
|
|
info = fetcher.get_url_info("https://pubmed.ncbi.nlm.nih.gov/12345678")
|
|
assert info["url_type"] == "pubmed"
|
|
assert info["extracted_id"] == "12345678"
|
|
|
|
# With trailing slash
|
|
info = fetcher.get_url_info("https://pubmed.ncbi.nlm.nih.gov/12345678/")
|
|
assert info["url_type"] == "pubmed"
|
|
|
|
# Old format
|
|
info = fetcher.get_url_info(
|
|
"https://www.ncbi.nlm.nih.gov/pubmed/12345678"
|
|
)
|
|
assert info["url_type"] == "pubmed"
|
|
|
|
def test_pmc_url_variants(self):
|
|
"""Test various PMC URL formats."""
|
|
fetcher = ContentFetcher()
|
|
|
|
# Standard PMC
|
|
info = fetcher.get_url_info(
|
|
"https://www.ncbi.nlm.nih.gov/pmc/articles/PMC1234567/"
|
|
)
|
|
assert info["url_type"] == "pmc"
|
|
assert info["extracted_id"] == "PMC1234567"
|
|
|
|
# Europe PMC
|
|
info = fetcher.get_url_info("https://europepmc.org/article/PMC/1234567")
|
|
assert info["url_type"] == "pmc"
|
|
|
|
def test_semantic_scholar_url_variants(self):
|
|
"""Test Semantic Scholar URL formats."""
|
|
fetcher = ContentFetcher()
|
|
|
|
# With paper title in URL
|
|
info = fetcher.get_url_info(
|
|
"https://www.semanticscholar.org/paper/Attention-Is-All-You-Need-Vaswani-Shazeer/abcdef1234567890abcdef1234567890abcdef12"
|
|
)
|
|
assert info["url_type"] == "semantic_scholar"
|
|
|
|
# API URL
|
|
info = fetcher.get_url_info(
|
|
"https://api.semanticscholar.org/v1/paper/abcd1234"
|
|
)
|
|
assert info["url_type"] == "semantic_scholar"
|
|
|
|
def test_biorxiv_medrxiv_url_variants(self):
|
|
"""Test bioRxiv and medRxiv URL formats."""
|
|
fetcher = ContentFetcher()
|
|
|
|
# bioRxiv
|
|
info = fetcher.get_url_info(
|
|
"https://www.biorxiv.org/content/10.1101/2021.01.01.123456v1"
|
|
)
|
|
assert info["url_type"] == "biorxiv"
|
|
|
|
# medRxiv
|
|
info = fetcher.get_url_info(
|
|
"https://www.medrxiv.org/content/10.1101/2021.01.01.123456v1"
|
|
)
|
|
assert info["url_type"] == "medrxiv"
|
|
|
|
def test_doi_url_variants(self):
|
|
"""Test DOI URL formats."""
|
|
fetcher = ContentFetcher()
|
|
|
|
# doi.org
|
|
info = fetcher.get_url_info("https://doi.org/10.1038/nature12373")
|
|
assert info["url_type"] == "doi"
|
|
assert "10.1038" in info["extracted_id"]
|
|
|
|
# dx.doi.org
|
|
info = fetcher.get_url_info(
|
|
"https://dx.doi.org/10.1126/science.1234567"
|
|
)
|
|
assert info["url_type"] == "doi"
|
|
|
|
|
|
class TestDownloadContentEdgeCases:
|
|
"""Test edge cases and error handling."""
|
|
|
|
@patch(
|
|
"local_deep_research.content_fetcher.fetcher.ContentFetcher._get_downloader"
|
|
)
|
|
def test_empty_url(self, mock_get_downloader):
|
|
"""Test handling of empty URL."""
|
|
fetcher = ContentFetcher()
|
|
# Empty URL should still try to classify and fetch
|
|
info = fetcher.get_url_info("")
|
|
assert info["url_type"] == "html" # Falls back to HTML
|
|
|
|
@patch(
|
|
"local_deep_research.content_fetcher.fetcher.ContentFetcher._get_downloader"
|
|
)
|
|
def test_malformed_url(self, mock_get_downloader):
|
|
"""Test handling of malformed URL."""
|
|
mock_downloader = MagicMock()
|
|
mock_downloader.download_with_result.side_effect = ValueError(
|
|
"Invalid URL"
|
|
)
|
|
mock_get_downloader.return_value = mock_downloader
|
|
|
|
fetcher = ContentFetcher()
|
|
result = fetcher.fetch("not-a-valid-url")
|
|
|
|
assert result["status"] == "error"
|
|
|
|
@patch(
|
|
"local_deep_research.content_fetcher.fetcher.ContentFetcher._get_downloader"
|
|
)
|
|
def test_binary_pdf_content(self, mock_get_downloader):
|
|
"""Test handling of binary PDF content that can't be decoded as UTF-8."""
|
|
# Simulate PDF bytes that fail UTF-8 decoding
|
|
pdf_bytes = b"%PDF-1.4\x00\x01\x02\x03binary content"
|
|
|
|
mock_downloader = MagicMock()
|
|
mock_downloader.download_with_result.return_value = DownloadResult(
|
|
content=pdf_bytes,
|
|
is_success=True,
|
|
)
|
|
mock_downloader.get_metadata.return_value = {}
|
|
mock_get_downloader.return_value = mock_downloader
|
|
|
|
# Mock the PDF text extraction to return extracted text
|
|
with patch(
|
|
"local_deep_research.research_library.downloaders.base.BaseDownloader.extract_text_from_pdf"
|
|
) as mock_extract:
|
|
mock_extract.return_value = "Extracted PDF text content"
|
|
|
|
fetcher = ContentFetcher()
|
|
result = fetcher.fetch("https://example.com/paper.pdf")
|
|
|
|
# Should attempt PDF extraction
|
|
assert result["status"] == "success" or result["status"] == "error"
|
|
|
|
@patch(
|
|
"local_deep_research.content_fetcher.fetcher.ContentFetcher._get_downloader"
|
|
)
|
|
def test_rate_limited_response(self, mock_get_downloader):
|
|
"""Test handling of rate-limited responses."""
|
|
mock_downloader = MagicMock()
|
|
mock_downloader.download_with_result.return_value = DownloadResult(
|
|
content=None,
|
|
is_success=False,
|
|
skip_reason="Rate limited - too many requests (429)",
|
|
)
|
|
mock_get_downloader.return_value = mock_downloader
|
|
|
|
fetcher = ContentFetcher()
|
|
result = fetcher.fetch("https://arxiv.org/abs/2301.12345")
|
|
|
|
assert result["status"] == "error"
|
|
assert "rate" in result["error"].lower() or "429" in result["error"]
|
|
|
|
@patch(
|
|
"local_deep_research.content_fetcher.fetcher.ContentFetcher._get_downloader"
|
|
)
|
|
def test_subscription_required(self, mock_get_downloader):
|
|
"""Test handling of paywalled content."""
|
|
mock_downloader = MagicMock()
|
|
mock_downloader.download_with_result.return_value = DownloadResult(
|
|
content=None,
|
|
is_success=False,
|
|
skip_reason="Subscription required to access full text",
|
|
)
|
|
mock_get_downloader.return_value = mock_downloader
|
|
|
|
fetcher = ContentFetcher()
|
|
result = fetcher.fetch("https://nature.com/articles/paywalled")
|
|
|
|
assert result["status"] == "error"
|
|
assert "subscription" in result["error"].lower()
|
|
|
|
|
|
class TestDownloadContentCaching:
|
|
"""Test downloader caching behavior."""
|
|
|
|
def test_downloader_reuse(self):
|
|
"""Test that downloaders are cached and reused."""
|
|
fetcher = ContentFetcher()
|
|
|
|
# Get HTML downloader twice
|
|
d1 = fetcher._get_downloader(
|
|
__import__(
|
|
"local_deep_research.content_fetcher.url_classifier",
|
|
fromlist=["URLType"],
|
|
).URLType.HTML
|
|
)
|
|
d2 = fetcher._get_downloader(
|
|
__import__(
|
|
"local_deep_research.content_fetcher.url_classifier",
|
|
fromlist=["URLType"],
|
|
).URLType.HTML
|
|
)
|
|
|
|
# Should be the same instance
|
|
assert d1 is d2
|
|
|
|
def test_different_downloaders_for_different_types(self):
|
|
"""Test that different URL types get different downloaders."""
|
|
from local_deep_research.content_fetcher.url_classifier import URLType
|
|
|
|
fetcher = ContentFetcher()
|
|
|
|
html_downloader = fetcher._get_downloader(URLType.HTML)
|
|
# Other downloaders may not be available, but HTML should always work
|
|
assert html_downloader is not None
|