Files
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

322 lines
12 KiB
Python

"""Extended tests for domain_classifier/classifier.py - targeting untested paths.
Covers:
- classify_all_domains() - completely untested (lines 276-407)
- classify_domain() with force_update=True
- get_categories_summary() - subcategory aggregation
- _get_domain_samples() edge cases
"""
from unittest.mock import MagicMock, Mock, patch
# ── classify_all_domains ──────────────────────────────────────────
class TestClassifyAllDomains:
"""Tests for classify_all_domains (lines 276-407, entirely untested)."""
def _make_classifier(self):
from local_deep_research.domain_classifier.classifier import (
DomainClassifier,
)
return DomainClassifier(username="testuser", settings_snapshot={})
def test_empty_database_returns_zero_total(self):
"""No resources → total=0, no processing."""
classifier = self._make_classifier()
mock_session = MagicMock()
mock_session.query.return_value.distinct.return_value.all.return_value = []
with patch(
"local_deep_research.domain_classifier.classifier.get_user_db_session"
) as mock_gs:
mock_gs.return_value.__enter__ = Mock(return_value=mock_session)
mock_gs.return_value.__exit__ = Mock(return_value=False)
result = classifier.classify_all_domains()
assert result["total"] == 0
assert result["classified"] == 0
assert result["failed"] == 0
assert result["skipped"] == 0
def test_extracts_domains_from_urls(self):
"""Domains are extracted and normalized from resource URLs."""
classifier = self._make_classifier()
mock_session = MagicMock()
mock_session.query.return_value.distinct.return_value.all.return_value = [
("https://www.example.com/page1",),
("https://www.example.com/page2",),
("https://other.org/doc",),
]
# For the domain query in classify_all_domains, we need to handle
# the subsequent query for DomainClassification filter_by
mock_session.query.return_value.filter_by.return_value.first.return_value = None
with (
patch(
"local_deep_research.domain_classifier.classifier.get_user_db_session"
) as mock_gs,
patch.object(classifier, "classify_domain", return_value=None),
):
mock_gs.return_value.__enter__ = Mock(return_value=mock_session)
mock_gs.return_value.__exit__ = Mock(return_value=False)
result = classifier.classify_all_domains(force_update=True)
# www.example.com → example.com (deduplicated), other.org → 2 unique domains
assert result["total"] == 2
def test_progress_callback_invoked(self):
"""Progress callback is called for each domain with percentage."""
classifier = self._make_classifier()
mock_session = MagicMock()
mock_session.query.return_value.distinct.return_value.all.return_value = [
("https://a.com/page",),
("https://b.com/page",),
]
callback = Mock()
with (
patch(
"local_deep_research.domain_classifier.classifier.get_user_db_session"
) as mock_gs,
patch.object(classifier, "classify_domain", return_value=None),
):
mock_gs.return_value.__enter__ = Mock(return_value=mock_session)
mock_gs.return_value.__exit__ = Mock(return_value=False)
classifier.classify_all_domains(
force_update=True, progress_callback=callback
)
assert callback.call_count == 2
first_call = callback.call_args_list[0][0][0]
assert "current" in first_call
assert "total" in first_call
assert "percentage" in first_call
def test_skips_already_classified_without_force(self):
"""Without force_update, already-classified domains are skipped."""
classifier = self._make_classifier()
mock_session = MagicMock()
mock_session.query.return_value.distinct.return_value.all.return_value = [
("https://known.com/page",),
]
existing = Mock()
existing.category = "Technology"
existing.subcategory = "Software Development"
mock_session.query.return_value.filter_by.return_value.first.return_value = existing
with patch(
"local_deep_research.domain_classifier.classifier.get_user_db_session"
) as mock_gs:
mock_gs.return_value.__enter__ = Mock(return_value=mock_session)
mock_gs.return_value.__exit__ = Mock(return_value=False)
result = classifier.classify_all_domains(force_update=False)
assert result["skipped"] == 1
assert result["classified"] == 0
def test_successful_classification_counted(self):
"""Successful classify_domain calls increment 'classified' counter."""
classifier = self._make_classifier()
mock_session = MagicMock()
mock_session.query.return_value.distinct.return_value.all.return_value = [
("https://new-site.com/page",),
]
mock_classification = Mock()
mock_classification.category = "Technology"
mock_classification.subcategory = "Cloud Service"
mock_classification.confidence = 0.9
with (
patch(
"local_deep_research.domain_classifier.classifier.get_user_db_session"
) as mock_gs,
patch.object(
classifier,
"classify_domain",
return_value=mock_classification,
),
):
mock_gs.return_value.__enter__ = Mock(return_value=mock_session)
mock_gs.return_value.__exit__ = Mock(return_value=False)
result = classifier.classify_all_domains(force_update=True)
assert result["classified"] == 1
assert result["domains"][0]["status"] == "classified"
assert result["domains"][0]["category"] == "Technology"
def test_failed_classification_counted(self):
"""Failed classify_domain calls increment 'failed' counter."""
classifier = self._make_classifier()
mock_session = MagicMock()
mock_session.query.return_value.distinct.return_value.all.return_value = [
("https://bad-site.com/page",),
]
with (
patch(
"local_deep_research.domain_classifier.classifier.get_user_db_session"
) as mock_gs,
patch.object(classifier, "classify_domain", return_value=None),
):
mock_gs.return_value.__enter__ = Mock(return_value=mock_session)
mock_gs.return_value.__exit__ = Mock(return_value=False)
result = classifier.classify_all_domains(force_update=True)
assert result["failed"] == 1
assert result["domains"][0]["status"] == "failed"
def test_exception_in_domain_increments_failed(self):
"""Exception during single domain classification is caught and counted."""
classifier = self._make_classifier()
mock_session = MagicMock()
mock_session.query.return_value.distinct.return_value.all.return_value = [
("https://crash.com/page",),
]
with (
patch(
"local_deep_research.domain_classifier.classifier.get_user_db_session"
) as mock_gs,
patch.object(
classifier,
"classify_domain",
side_effect=RuntimeError("LLM error"),
),
):
mock_gs.return_value.__enter__ = Mock(return_value=mock_session)
mock_gs.return_value.__exit__ = Mock(return_value=False)
result = classifier.classify_all_domains(force_update=True)
assert result["failed"] == 1
assert result["domains"][0]["status"] == "failed"
def test_invalid_url_skipped_gracefully(self):
"""URLs that fail parsing are skipped without crashing."""
classifier = self._make_classifier()
mock_session = MagicMock()
mock_session.query.return_value.distinct.return_value.all.return_value = [
("not a valid url",),
(None,),
("https://valid.com/page",),
]
with (
patch(
"local_deep_research.domain_classifier.classifier.get_user_db_session"
) as mock_gs,
patch.object(classifier, "classify_domain", return_value=None),
):
mock_gs.return_value.__enter__ = Mock(return_value=mock_session)
mock_gs.return_value.__exit__ = Mock(return_value=False)
result = classifier.classify_all_domains(force_update=True)
# "not a valid url" has empty netloc, None is skipped → only valid.com
assert result["total"] == 1
# ── get_categories_summary: subcategory aggregation ───────────────
class TestGetCategoriesSummary:
"""Tests for get_categories_summary subcategory aggregation (lines 449-488)."""
def _make_classifier(self):
from local_deep_research.domain_classifier.classifier import (
DomainClassifier,
)
return DomainClassifier(username="testuser")
def test_multiple_domains_same_category(self):
"""Multiple domains in the same category are aggregated."""
classifier = self._make_classifier()
c1 = Mock(
domain="a.com",
category="Technology",
subcategory="Cloud",
confidence=0.9,
)
c2 = Mock(
domain="b.com",
category="Technology",
subcategory="SaaS",
confidence=0.8,
)
mock_session = MagicMock()
mock_session.query.return_value.all.return_value = [c1, c2]
with patch(
"local_deep_research.domain_classifier.classifier.get_user_db_session"
) as mock_gs:
mock_gs.return_value.__enter__ = Mock(return_value=mock_session)
mock_gs.return_value.__exit__ = Mock(return_value=False)
result = classifier.get_categories_summary()
assert result["Technology"]["count"] == 2
assert result["Technology"]["subcategories"]["Cloud"] == 1
assert result["Technology"]["subcategories"]["SaaS"] == 1
def test_none_subcategory_skipped(self):
"""None subcategory does not appear in subcategories dict."""
classifier = self._make_classifier()
c1 = Mock(
domain="x.com", category="Other", subcategory=None, confidence=0.5
)
mock_session = MagicMock()
mock_session.query.return_value.all.return_value = [c1]
with patch(
"local_deep_research.domain_classifier.classifier.get_user_db_session"
) as mock_gs:
mock_gs.return_value.__enter__ = Mock(return_value=mock_session)
mock_gs.return_value.__exit__ = Mock(return_value=False)
result = classifier.get_categories_summary()
assert result["Other"]["count"] == 1
assert result["Other"]["subcategories"] == {}
def test_empty_database_returns_empty(self):
"""No classifications → empty summary."""
classifier = self._make_classifier()
mock_session = MagicMock()
mock_session.query.return_value.all.return_value = []
with patch(
"local_deep_research.domain_classifier.classifier.get_user_db_session"
) as mock_gs:
mock_gs.return_value.__enter__ = Mock(return_value=mock_session)
mock_gs.return_value.__exit__ = Mock(return_value=False)
result = classifier.get_categories_summary()
assert result == {}