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
368 lines
12 KiB
Python
368 lines
12 KiB
Python
"""Tests for domain classifier."""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from local_deep_research.domain_classifier.classifier import (
|
|
DOMAIN_CATEGORIES,
|
|
DomainClassifier,
|
|
)
|
|
|
|
|
|
class TestDomainCategories:
|
|
"""Tests for DOMAIN_CATEGORIES constant."""
|
|
|
|
def test_has_expected_main_categories(self):
|
|
"""Should have all expected main categories."""
|
|
expected = [
|
|
"Academic & Research",
|
|
"News & Media",
|
|
"Reference & Documentation",
|
|
"Social & Community",
|
|
"Business & Commerce",
|
|
"Technology",
|
|
"Government & Organization",
|
|
"Entertainment & Lifestyle",
|
|
"Professional & Industry",
|
|
"Other",
|
|
]
|
|
for category in expected:
|
|
assert category in DOMAIN_CATEGORIES
|
|
|
|
def test_each_category_has_subcategories(self):
|
|
"""Should have subcategories for each main category."""
|
|
for category, subcategories in DOMAIN_CATEGORIES.items():
|
|
assert isinstance(subcategories, list)
|
|
assert len(subcategories) > 0
|
|
|
|
def test_other_includes_unknown(self):
|
|
"""Other category should include Unknown subcategory."""
|
|
assert "Unknown" in DOMAIN_CATEGORIES["Other"]
|
|
|
|
|
|
class TestDomainClassifierInit:
|
|
"""Tests for DomainClassifier initialization."""
|
|
|
|
def test_stores_username(self):
|
|
"""Should store username."""
|
|
classifier = DomainClassifier(username="testuser")
|
|
assert classifier.username == "testuser"
|
|
|
|
def test_stores_settings_snapshot(self):
|
|
"""Should store settings snapshot."""
|
|
snapshot = {"key": "value"}
|
|
classifier = DomainClassifier(
|
|
username="testuser", settings_snapshot=snapshot
|
|
)
|
|
assert classifier.settings_snapshot == snapshot
|
|
|
|
def test_llm_initially_none(self):
|
|
"""Should have LLM as None initially."""
|
|
classifier = DomainClassifier(username="testuser")
|
|
assert classifier.llm is None
|
|
|
|
|
|
class TestGetLlm:
|
|
"""Tests for _get_llm method."""
|
|
|
|
def test_creates_llm_on_first_call(self):
|
|
"""Should create LLM on first call."""
|
|
mock_llm = MagicMock()
|
|
classifier = DomainClassifier(username="testuser")
|
|
|
|
with patch(
|
|
"local_deep_research.domain_classifier.classifier.get_llm",
|
|
return_value=mock_llm,
|
|
):
|
|
result = classifier._get_llm()
|
|
|
|
assert result is mock_llm
|
|
assert classifier.llm is mock_llm
|
|
|
|
def test_reuses_existing_llm(self):
|
|
"""Should reuse existing LLM on subsequent calls."""
|
|
mock_llm = MagicMock()
|
|
classifier = DomainClassifier(username="testuser")
|
|
classifier.llm = mock_llm
|
|
|
|
with patch(
|
|
"local_deep_research.domain_classifier.classifier.get_llm"
|
|
) as mock_get:
|
|
result = classifier._get_llm()
|
|
|
|
mock_get.assert_not_called()
|
|
assert result is mock_llm
|
|
|
|
def test_passes_settings_snapshot(self):
|
|
"""Should pass settings snapshot to get_llm."""
|
|
snapshot = {"key": "value"}
|
|
classifier = DomainClassifier(
|
|
username="testuser", settings_snapshot=snapshot
|
|
)
|
|
|
|
with patch(
|
|
"local_deep_research.domain_classifier.classifier.get_llm"
|
|
) as mock_get:
|
|
classifier._get_llm()
|
|
|
|
mock_get.assert_called_once_with(settings_snapshot=snapshot)
|
|
|
|
|
|
class TestBuildClassificationPrompt:
|
|
"""Tests for _build_classification_prompt method."""
|
|
|
|
def test_includes_domain(self):
|
|
"""Should include domain in prompt."""
|
|
classifier = DomainClassifier(username="testuser")
|
|
samples = []
|
|
prompt = classifier._build_classification_prompt("example.com", samples)
|
|
assert "example.com" in prompt
|
|
|
|
def test_includes_categories(self, sample_samples):
|
|
"""Should include categories in prompt."""
|
|
classifier = DomainClassifier(username="testuser")
|
|
prompt = classifier._build_classification_prompt(
|
|
"example.com", sample_samples
|
|
)
|
|
assert "News & Media" in prompt
|
|
assert "Technology" in prompt
|
|
|
|
def test_includes_samples(self, sample_samples):
|
|
"""Should include samples in prompt."""
|
|
classifier = DomainClassifier(username="testuser")
|
|
prompt = classifier._build_classification_prompt(
|
|
"example.com", sample_samples
|
|
)
|
|
assert "Article 1" in prompt
|
|
assert "Article 2" in prompt
|
|
|
|
def test_handles_empty_samples(self):
|
|
"""Should handle empty samples."""
|
|
classifier = DomainClassifier(username="testuser")
|
|
prompt = classifier._build_classification_prompt("example.com", [])
|
|
assert "No samples available" in prompt
|
|
|
|
def test_requests_json_response(self, sample_samples):
|
|
"""Should request JSON response format."""
|
|
classifier = DomainClassifier(username="testuser")
|
|
prompt = classifier._build_classification_prompt(
|
|
"example.com", sample_samples
|
|
)
|
|
assert "JSON" in prompt
|
|
assert "category" in prompt
|
|
assert "subcategory" in prompt
|
|
assert "confidence" in prompt
|
|
|
|
|
|
class TestClassifyDomain:
|
|
"""Tests for classify_domain method."""
|
|
|
|
def test_returns_existing_classification(self, mock_domain_classification):
|
|
"""Should return existing classification without reclassifying."""
|
|
classifier = DomainClassifier(username="testuser")
|
|
|
|
with patch(
|
|
"local_deep_research.domain_classifier.classifier.get_user_db_session"
|
|
) as mock_session_ctx:
|
|
mock_session = MagicMock()
|
|
mock_session_ctx.return_value.__enter__ = MagicMock(
|
|
return_value=mock_session
|
|
)
|
|
mock_session_ctx.return_value.__exit__ = MagicMock(
|
|
return_value=None
|
|
)
|
|
mock_session.query.return_value.filter_by.return_value.first.return_value = mock_domain_classification
|
|
|
|
result = classifier.classify_domain("example.com")
|
|
|
|
assert result is mock_domain_classification
|
|
|
|
def test_returns_none_on_error(self):
|
|
"""Should return None on error."""
|
|
classifier = DomainClassifier(username="testuser")
|
|
|
|
with patch(
|
|
"local_deep_research.domain_classifier.classifier.get_user_db_session"
|
|
) as mock_session_ctx:
|
|
mock_session_ctx.return_value.__enter__ = MagicMock(
|
|
side_effect=Exception("DB error")
|
|
)
|
|
|
|
result = classifier.classify_domain("example.com")
|
|
|
|
assert result is None
|
|
|
|
|
|
class TestGetClassification:
|
|
"""Tests for get_classification method."""
|
|
|
|
def test_returns_classification_when_exists(
|
|
self, mock_domain_classification
|
|
):
|
|
"""Should return classification when exists."""
|
|
classifier = DomainClassifier(username="testuser")
|
|
|
|
with patch(
|
|
"local_deep_research.domain_classifier.classifier.get_user_db_session"
|
|
) as mock_session_ctx:
|
|
mock_session = MagicMock()
|
|
mock_session_ctx.return_value.__enter__ = MagicMock(
|
|
return_value=mock_session
|
|
)
|
|
mock_session_ctx.return_value.__exit__ = MagicMock(
|
|
return_value=None
|
|
)
|
|
mock_session.query.return_value.filter_by.return_value.first.return_value = mock_domain_classification
|
|
|
|
result = classifier.get_classification("example.com")
|
|
|
|
assert result is mock_domain_classification
|
|
|
|
def test_returns_none_when_not_found(self):
|
|
"""Should return None when not found."""
|
|
classifier = DomainClassifier(username="testuser")
|
|
|
|
with patch(
|
|
"local_deep_research.domain_classifier.classifier.get_user_db_session"
|
|
) as mock_session_ctx:
|
|
mock_session = MagicMock()
|
|
mock_session_ctx.return_value.__enter__ = MagicMock(
|
|
return_value=mock_session
|
|
)
|
|
mock_session_ctx.return_value.__exit__ = MagicMock(
|
|
return_value=None
|
|
)
|
|
mock_session.query.return_value.filter_by.return_value.first.return_value = None
|
|
|
|
result = classifier.get_classification("unknown.com")
|
|
|
|
assert result is None
|
|
|
|
def test_returns_none_on_error(self):
|
|
"""Should return None on error."""
|
|
classifier = DomainClassifier(username="testuser")
|
|
|
|
with patch(
|
|
"local_deep_research.domain_classifier.classifier.get_user_db_session"
|
|
) as mock_session_ctx:
|
|
mock_session_ctx.return_value.__enter__ = MagicMock(
|
|
side_effect=Exception("DB error")
|
|
)
|
|
|
|
result = classifier.get_classification("example.com")
|
|
|
|
assert result is None
|
|
|
|
|
|
class TestGetAllClassifications:
|
|
"""Tests for get_all_classifications method."""
|
|
|
|
def test_returns_list_of_classifications(self, mock_domain_classification):
|
|
"""Should return list of classifications."""
|
|
classifier = DomainClassifier(username="testuser")
|
|
|
|
with patch(
|
|
"local_deep_research.domain_classifier.classifier.get_user_db_session"
|
|
) as mock_session_ctx:
|
|
mock_session = MagicMock()
|
|
mock_session_ctx.return_value.__enter__ = MagicMock(
|
|
return_value=mock_session
|
|
)
|
|
mock_session_ctx.return_value.__exit__ = MagicMock(
|
|
return_value=None
|
|
)
|
|
mock_session.query.return_value.order_by.return_value.all.return_value = [
|
|
mock_domain_classification
|
|
]
|
|
|
|
result = classifier.get_all_classifications()
|
|
|
|
assert len(result) == 1
|
|
assert result[0] is mock_domain_classification
|
|
|
|
def test_returns_empty_list_on_error(self):
|
|
"""Should return empty list on error."""
|
|
classifier = DomainClassifier(username="testuser")
|
|
|
|
with patch(
|
|
"local_deep_research.domain_classifier.classifier.get_user_db_session"
|
|
) as mock_session_ctx:
|
|
mock_session_ctx.return_value.__enter__ = MagicMock(
|
|
side_effect=Exception("DB error")
|
|
)
|
|
|
|
result = classifier.get_all_classifications()
|
|
|
|
assert result == []
|
|
|
|
|
|
class TestGetCategoriesSummary:
|
|
"""Tests for get_categories_summary method."""
|
|
|
|
def test_returns_summary_dict(self, mock_domain_classification):
|
|
"""Should return summary dictionary."""
|
|
classifier = DomainClassifier(username="testuser")
|
|
|
|
with patch(
|
|
"local_deep_research.domain_classifier.classifier.get_user_db_session"
|
|
) as mock_session_ctx:
|
|
mock_session = MagicMock()
|
|
mock_session_ctx.return_value.__enter__ = MagicMock(
|
|
return_value=mock_session
|
|
)
|
|
mock_session_ctx.return_value.__exit__ = MagicMock(
|
|
return_value=None
|
|
)
|
|
mock_session.query.return_value.all.return_value = [
|
|
mock_domain_classification
|
|
]
|
|
|
|
result = classifier.get_categories_summary()
|
|
|
|
assert "News & Media" in result
|
|
assert result["News & Media"]["count"] == 1
|
|
|
|
def test_returns_empty_dict_on_error(self):
|
|
"""Should return empty dict on error."""
|
|
classifier = DomainClassifier(username="testuser")
|
|
|
|
with patch(
|
|
"local_deep_research.domain_classifier.classifier.get_user_db_session"
|
|
) as mock_session_ctx:
|
|
mock_session_ctx.return_value.__enter__ = MagicMock(
|
|
side_effect=Exception("DB error")
|
|
)
|
|
|
|
result = classifier.get_categories_summary()
|
|
|
|
assert result == {}
|
|
|
|
|
|
class TestUrlparseValueErrorHandling:
|
|
"""Tests for ValueError handling in urlparse (PR #2012).
|
|
|
|
PR #2012 changed bare `except:` to `except ValueError:` when
|
|
parsing URLs in classify_all_domains.
|
|
"""
|
|
|
|
def test_value_error_from_urlparse_caught(self):
|
|
"""ValueError from urlparse is caught with continue."""
|
|
from urllib.parse import urlparse
|
|
|
|
# URLs that could cause issues
|
|
test_urls = ["https://valid.com/path", "", "not-a-url"]
|
|
domains = set()
|
|
|
|
for url in test_urls:
|
|
if url:
|
|
try:
|
|
parsed = urlparse(url)
|
|
domain = parsed.netloc.lower()
|
|
if domain.startswith("www."):
|
|
domain = domain[4:]
|
|
if domain:
|
|
domains.add(domain)
|
|
except ValueError:
|
|
continue
|
|
|
|
assert "valid.com" in domains
|