项目文件夹

文件
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

558 行
21 KiB
Python

"""
Extended Tests for Failure Classifier System
Phase 16: Download Management Deep Coverage - Failure Classification Tests
Tests failure classification, domain-specific logic, and pattern matching.
"""
from datetime import datetime, timedelta, UTC
from local_deep_research.library.download_management.failure_classifier import (
PermanentFailure,
TemporaryFailure,
RateLimitFailure,
FailureClassifier,
)
class TestFailureClassification:
"""Tests for HTTP status code and error type classification"""
def test_classify_http_404_not_found(self):
"""Test 404 status code is classified as permanent failure"""
classifier = FailureClassifier()
failure = classifier.classify_failure(
error_type="http_error",
status_code=404,
url="https://example.com/resource.pdf",
)
assert isinstance(failure, PermanentFailure)
assert failure.error_type == "not_found"
assert "404" in failure.message
assert failure.is_permanent() is True
def test_classify_http_403_forbidden(self):
"""Test 403 status code is classified as permanent failure"""
classifier = FailureClassifier()
failure = classifier.classify_failure(
error_type="http_error",
status_code=403,
url="https://example.com/forbidden.pdf",
)
assert isinstance(failure, PermanentFailure)
assert failure.error_type == "forbidden"
assert "403" in failure.message
assert failure.is_permanent() is True
def test_classify_http_429_rate_limit(self):
"""Test 429 status code is classified as rate limit failure"""
classifier = FailureClassifier()
failure = classifier.classify_failure(
error_type="http_error",
status_code=429,
url="https://arxiv.org/pdf/2301.00001.pdf",
)
assert isinstance(failure, RateLimitFailure)
assert failure.error_type == "rate_limited"
assert failure.is_permanent() is False
assert failure.domain == "arxiv.org"
def test_classify_http_500_server_error(self):
"""Test 500 status code is classified as temporary (unknown error)"""
classifier = FailureClassifier()
failure = classifier.classify_failure(
error_type="http_error",
status_code=500,
url="https://example.com/error.pdf",
)
# 500 doesn't have a specific handler, falls through to unknown
assert isinstance(failure, TemporaryFailure)
assert failure.is_permanent() is False
def test_classify_http_503_service_unavailable(self):
"""Test 503 status code is classified as temporary server error"""
classifier = FailureClassifier()
failure = classifier.classify_failure(
error_type="http_error",
status_code=503,
url="https://example.com/unavailable.pdf",
)
assert isinstance(failure, TemporaryFailure)
assert failure.error_type == "server_error"
assert "503" in failure.message
assert failure.is_permanent() is False
# Should have 1-hour cooldown
assert failure.retry_after == timedelta(hours=1)
def test_classify_timeout_error(self):
"""Test timeout errors are classified as temporary failures"""
classifier = FailureClassifier()
failure = classifier.classify_failure(
error_type="TimeoutError",
url="https://example.com/slow.pdf",
)
assert isinstance(failure, TemporaryFailure)
assert failure.error_type == "timeout"
assert failure.is_permanent() is False
assert failure.retry_after == timedelta(minutes=30)
def test_classify_connection_refused(self):
"""Test connection errors are classified as temporary failures"""
classifier = FailureClassifier()
failure = classifier.classify_failure(
error_type="ConnectionError",
url="https://example.com/unreachable.pdf",
details="Connection refused",
)
assert isinstance(failure, TemporaryFailure)
assert failure.error_type == "network_error"
assert failure.is_permanent() is False
assert failure.retry_after == timedelta(minutes=5)
def test_classify_dns_resolution_failure(self):
"""Test DNS resolution failures are classified as network errors"""
classifier = FailureClassifier()
failure = classifier.classify_failure(
error_type="NetworkError",
url="https://nonexistent.invalid/file.pdf",
details="DNS resolution failed",
)
assert isinstance(failure, TemporaryFailure)
assert failure.error_type == "network_error"
assert failure.is_permanent() is False
def test_classify_ssl_certificate_error(self):
"""Test SSL errors fall to unknown temporary failure"""
classifier = FailureClassifier()
failure = classifier.classify_failure(
error_type="SSLError",
url="https://expired-cert.example.com/file.pdf",
details="SSL certificate verification failed",
)
# SSL errors don't have specific handling, fall to unknown
assert isinstance(failure, TemporaryFailure)
assert failure.is_permanent() is False
def test_classify_arxiv_recaptcha_detection(self):
"""Test arXiv reCAPTCHA detection results in 3-day cooldown"""
classifier = FailureClassifier()
failure = classifier.classify_failure(
error_type="ArxivError",
url="https://arxiv.org/pdf/2301.00001.pdf",
details="reCAPTCHA protection detected",
)
assert isinstance(failure, TemporaryFailure)
assert failure.error_type == "recaptcha_protection"
assert failure.retry_after == timedelta(days=3)
assert failure.is_permanent() is False
def test_classify_incompatible_format_pdf(self):
"""Test incompatible PDF format is classified as permanent"""
classifier = FailureClassifier()
failure = classifier.classify_failure(
error_type="ArxivError",
url="https://arxiv.org/abs/2301.00001",
details="not a PDF file",
)
assert isinstance(failure, PermanentFailure)
assert failure.error_type == "incompatible_format"
assert failure.is_permanent() is True
def test_classify_incompatible_format_html(self):
"""Test HTML instead of PDF is classified as permanent"""
classifier = FailureClassifier()
failure = classifier.classify_failure(
error_type="ArxivError",
url="https://arxiv.org/abs/2301.00001",
details="Received HTML content instead of PDF",
)
assert isinstance(failure, PermanentFailure)
assert failure.error_type == "incompatible_format"
assert failure.is_permanent() is True
def test_classify_permanent_failure_detection(self):
"""Test 410 Gone is classified as permanent"""
classifier = FailureClassifier()
failure = classifier.classify_failure(
error_type="http_error",
status_code=410,
url="https://example.com/removed.pdf",
)
assert isinstance(failure, PermanentFailure)
assert failure.error_type == "gone"
assert "410" in failure.message
assert failure.is_permanent() is True
def test_classify_temporary_failure_detection(self):
"""Test temporary failures are properly detected"""
classifier = FailureClassifier()
failure = classifier.classify_failure(
error_type="temporary_issue",
url="https://example.com/temp.pdf",
)
assert isinstance(failure, TemporaryFailure)
assert failure.is_permanent() is False
assert failure.retry_after is not None
def test_classify_unknown_error_fallback(self):
"""Test unknown errors default to temporary 1-hour cooldown"""
classifier = FailureClassifier()
failure = classifier.classify_failure(
error_type="WeirdUnknownError",
url="https://example.com/mystery.pdf",
details="Something unusual happened",
)
assert isinstance(failure, TemporaryFailure)
assert failure.error_type == "unknown_error"
assert failure.retry_after == timedelta(hours=1)
assert failure.is_permanent() is False
class TestDomainSpecificLogic:
"""Tests for domain-specific cooldown and handling logic"""
def test_domain_cooldown_arxiv(self):
"""Test arXiv-specific rate limit cooldown of 6 hours"""
failure = RateLimitFailure("arxiv.org")
assert failure.domain == "arxiv.org"
assert failure.retry_after == timedelta(hours=6)
assert "arxiv.org" in failure.message
def test_domain_cooldown_semantic_scholar(self):
"""Test Semantic Scholar rate limit cooldown of 4 hours"""
failure = RateLimitFailure("semanticscholar.org")
assert failure.domain == "semanticscholar.org"
assert failure.retry_after == timedelta(hours=4)
def test_domain_cooldown_pubmed(self):
"""Test PubMed rate limit cooldown of 2 hours"""
failure = RateLimitFailure("pubmed.ncbi.nlm.nih.gov")
assert failure.domain == "pubmed.ncbi.nlm.nih.gov"
assert failure.retry_after == timedelta(hours=2)
def test_domain_cooldown_ieee(self):
"""Test IEEE (unknown domain) gets default 1-hour cooldown"""
failure = RateLimitFailure("ieeexplore.ieee.org")
assert failure.domain == "ieeexplore.ieee.org"
# IEEE not in specific domains, falls to default
assert failure.retry_after == timedelta(hours=1)
def test_domain_cooldown_springer(self):
"""Test Springer (unknown domain) gets default cooldown"""
failure = RateLimitFailure("link.springer.com")
assert failure.domain == "link.springer.com"
assert failure.retry_after == timedelta(hours=1)
def test_domain_cooldown_default(self):
"""Test unknown domains get default 1-hour cooldown"""
failure = RateLimitFailure("unknown-domain.com")
assert failure.domain == "unknown-domain.com"
assert failure.retry_after == timedelta(hours=1)
def test_rate_limit_failure_initialization(self):
"""Test RateLimitFailure initializes with proper attributes"""
failure = RateLimitFailure("biorxiv.org", "Too many requests")
assert failure.error_type == "rate_limited"
assert failure.domain == "biorxiv.org"
assert "biorxiv.org" in failure.message
assert "Too many requests" in failure.message
assert failure.retry_after == timedelta(hours=6)
def test_pattern_matching_null_values(self):
"""Test classifier handles None/null values gracefully"""
classifier = FailureClassifier()
failure = classifier.classify_failure(
error_type="",
status_code=None,
url="",
details="",
)
# Should default to unknown temporary failure
assert isinstance(failure, TemporaryFailure)
assert failure.is_permanent() is False
def test_pattern_matching_mixed_patterns(self):
"""Test classifier with mixed patterns in details"""
classifier = FailureClassifier()
failure = classifier.classify_failure(
error_type="arxiv_download_error",
url="https://arxiv.org/pdf/test.pdf",
details="Captcha required - please solve the reCAPTCHA",
)
assert isinstance(failure, TemporaryFailure)
assert failure.error_type == "recaptcha_protection"
assert failure.retry_after == timedelta(days=3)
def test_custom_domain_handling(self):
"""Test custom domain with details in RateLimitFailure"""
failure = RateLimitFailure("researchgate.net", "Account rate limited")
assert failure.domain == "researchgate.net"
assert failure.retry_after == timedelta(hours=12)
assert "Account rate limited" in failure.message
def test_failure_message_extraction(self):
"""Test failure message is properly set"""
failure = PermanentFailure("test_error", "This is a test message")
assert failure.message == "This is a test message"
assert failure.error_type == "test_error"
def test_failure_type_enumeration(self):
"""Test various failure types can be created"""
# Permanent types
perm1 = PermanentFailure("not_found", "Not found")
perm2 = PermanentFailure("forbidden", "Forbidden")
perm3 = PermanentFailure("gone", "Gone")
perm4 = PermanentFailure("incompatible_format", "Wrong format")
assert all(f.is_permanent() for f in [perm1, perm2, perm3, perm4])
# Temporary types
temp1 = TemporaryFailure("timeout", "Timeout", timedelta(minutes=30))
temp2 = TemporaryFailure(
"network_error", "Network", timedelta(minutes=5)
)
assert not any(f.is_permanent() for f in [temp1, temp2])
def test_retry_eligible_classification(self):
"""Test correct identification of retry-eligible failures"""
# audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: use_zero_cooldown_instead).
temp_failure = TemporaryFailure(
"temp", "Temporary", timedelta(seconds=1)
)
perm_failure = PermanentFailure("perm", "Permanent")
# Permanent should always be permanent; temporary should not
assert temp_failure.is_permanent() is False
assert perm_failure.is_permanent() is True
def test_permanent_failure_reasons(self):
"""Test permanent failure has expected attributes"""
failure = PermanentFailure("test_permanent", "Test permanent failure")
assert failure.retry_after is None
assert failure.is_permanent() is True
class TestBaseFailureMethods:
"""Tests for BaseFailure base class methods"""
def test_is_permanent_for_permanent_failure(self):
"""Test is_permanent returns True for PermanentFailure"""
failure = PermanentFailure("test", "Test")
assert failure.is_permanent() is True
def test_is_permanent_for_temporary_failure(self):
"""Test is_permanent returns False for TemporaryFailure"""
failure = TemporaryFailure("test", "Test", timedelta(hours=1))
assert failure.is_permanent() is False
def test_created_at_timestamp(self):
"""Test created_at is set to current UTC time"""
before = datetime.now(UTC)
failure = TemporaryFailure("test", "Test", timedelta(hours=1))
after = datetime.now(UTC)
assert before <= failure.created_at <= after
class TestClassifyFromException:
"""Tests for classify_from_exception method"""
def test_classify_from_timeout_exception(self):
"""Test classification from TimeoutError exception"""
classifier = FailureClassifier()
exc = TimeoutError("Connection timed out")
failure = classifier.classify_from_exception(exc, "https://example.com")
assert isinstance(failure, TemporaryFailure)
assert failure.error_type == "timeout"
def test_classify_from_connection_exception(self):
"""Test classification from ConnectionError exception"""
classifier = FailureClassifier()
exc = ConnectionError("Connection refused")
failure = classifier.classify_from_exception(exc, "https://example.com")
assert isinstance(failure, TemporaryFailure)
assert failure.error_type == "network_error"
def test_classify_from_generic_exception(self):
"""Test classification from generic Exception"""
classifier = FailureClassifier()
exc = Exception("Something went wrong")
failure = classifier.classify_from_exception(exc, "https://example.com")
assert isinstance(failure, TemporaryFailure)
assert failure.is_permanent() is False
def test_classify_from_exception_with_empty_url(self):
"""Test classify_from_exception with empty URL"""
classifier = FailureClassifier()
exc = ValueError("Invalid data")
failure = classifier.classify_from_exception(exc, "")
assert isinstance(failure, TemporaryFailure)
assert failure.is_permanent() is False
def test_classify_from_exception_extracts_message(self):
"""Test exception message is included in details"""
classifier = FailureClassifier()
exc = RuntimeError("Specific runtime error message")
failure = classifier.classify_from_exception(exc, "https://example.com")
# The exception message should influence classification
assert failure.message is not None
class TestEdgeCases:
"""Tests for edge cases and boundary conditions"""
def test_case_insensitive_error_matching(self):
"""Test error matching is case-insensitive"""
classifier = FailureClassifier()
# Test uppercase TIMEOUT
failure1 = classifier.classify_failure(
error_type="TIMEOUT",
url="https://example.com",
)
# Test mixed case TimeOut
failure2 = classifier.classify_failure(
error_type="TimeOut",
url="https://example.com",
)
assert failure1.error_type == "timeout"
assert failure2.error_type == "timeout"
def test_arxiv_pattern_in_details(self):
"""Test arXiv detection from details field"""
classifier = FailureClassifier()
failure = classifier.classify_failure(
error_type="download_error",
url="https://example.com",
details="Error from arxiv - captcha detected",
)
assert failure.error_type == "recaptcha_protection"
def test_url_domain_extraction_for_rate_limit(self):
"""Test domain is correctly extracted from URL for rate limits"""
classifier = FailureClassifier()
failure = classifier.classify_failure(
error_type="http_error",
status_code=429,
url="https://api.semanticscholar.org/v1/paper/123",
)
assert isinstance(failure, RateLimitFailure)
assert failure.domain == "api.semanticscholar.org"
def test_url_with_port_number(self):
"""Test URL with port number is handled"""
classifier = FailureClassifier()
failure = classifier.classify_failure(
error_type="http_error",
status_code=429,
url="https://localhost:8080/api/download",
)
assert isinstance(failure, RateLimitFailure)
assert failure.domain == "localhost:8080"
def test_malformed_url_handling(self):
"""Test malformed URLs don't cause crashes"""
classifier = FailureClassifier()
failure = classifier.classify_failure(
error_type="http_error",
status_code=429,
url="not-a-valid-url",
)
# Should still create a failure object
assert isinstance(failure, RateLimitFailure)
def test_very_long_details_string(self):
"""Test handling of very long details strings"""
classifier = FailureClassifier()
long_details = "x" * 10000
failure = classifier.classify_failure(
error_type="error",
url="https://example.com",
details=long_details,
)
assert isinstance(failure, TemporaryFailure)
def test_unicode_in_error_message(self):
"""Test unicode characters in error messages"""
classifier = FailureClassifier()
failure = classifier.classify_failure(
error_type="error",
url="https://example.com",
details="Error: \u4e2d\u6587\u6d4b\u8bd5 (Chinese test)",
)
assert isinstance(failure, TemporaryFailure)
def test_html_pdf_exclusion(self):
"""Test HTML is flagged but application/pdf is allowed"""
classifier = FailureClassifier()
# This should NOT be classified as incompatible (has application/pdf)
failure1 = classifier.classify_failure(
error_type="ArxivError",
url="https://arxiv.org/pdf/test.pdf",
details="Content-Type: html with application/pdf fallback",
)
# failure1 should not be permanent incompatible format
assert (
failure1.error_type != "incompatible_format"
or not failure1.is_permanent()
)
# This SHOULD be classified as incompatible (no application/pdf)
failure2 = classifier.classify_failure(
error_type="ArxivError",
url="https://arxiv.org/abs/test",
details="Received HTML instead of expected format",
)
assert isinstance(failure2, PermanentFailure)
assert failure2.error_type == "incompatible_format"