""" Comprehensive tests for domain_classifier/classifier.py Tests cover: - DomainClassifier initialization - Domain categories - Classification logic - LLM integration """ import pytest from unittest.mock import Mock, patch, MagicMock class TestDomainCategories: """Tests for domain category definitions.""" def test_domain_categories_exist(self): """Test that domain categories are defined.""" from local_deep_research.domain_classifier.classifier import ( DOMAIN_CATEGORIES, ) assert isinstance(DOMAIN_CATEGORIES, dict) assert len(DOMAIN_CATEGORIES) > 0 def test_expected_categories_present(self): """Test that expected top-level categories exist.""" from local_deep_research.domain_classifier.classifier import ( DOMAIN_CATEGORIES, ) expected_categories = [ "Academic & Research", "News & Media", "Reference & Documentation", "Social & Community", "Business & Commerce", "Technology", "Government & Organization", "Other", ] for category in expected_categories: assert category in DOMAIN_CATEGORIES, ( f"Missing category: {category}" ) def test_academic_subcategories(self): """Test academic category subcategories.""" from local_deep_research.domain_classifier.classifier import ( DOMAIN_CATEGORIES, ) academic = DOMAIN_CATEGORIES.get("Academic & Research", []) assert "University/Education" in academic assert "Scientific Journal" in academic assert "Research Institution" in academic def test_news_subcategories(self): """Test news category subcategories.""" from local_deep_research.domain_classifier.classifier import ( DOMAIN_CATEGORIES, ) news = DOMAIN_CATEGORIES.get("News & Media", []) assert "General News" in news assert "Tech News" in news assert "Business News" in news def test_technology_subcategories(self): """Test technology category subcategories.""" from local_deep_research.domain_classifier.classifier import ( DOMAIN_CATEGORIES, ) tech = DOMAIN_CATEGORIES.get("Technology", []) assert "Software Development" in tech assert "Open Source Project" in tech assert "Cloud Service" in tech def test_other_includes_unknown(self): """Test that Other category includes Unknown.""" from local_deep_research.domain_classifier.classifier import ( DOMAIN_CATEGORIES, ) other = DOMAIN_CATEGORIES.get("Other", []) assert "Unknown" in other def test_all_categories_have_subcategories(self): """Test that all categories have at least one subcategory.""" from local_deep_research.domain_classifier.classifier import ( DOMAIN_CATEGORIES, ) for category, subcategories in DOMAIN_CATEGORIES.items(): assert len(subcategories) > 0, ( f"Category {category} has no subcategories" ) class TestDomainClassifierInit: """Tests for DomainClassifier initialization.""" def test_init_with_username(self): """Test initialization with username.""" from local_deep_research.domain_classifier.classifier import ( DomainClassifier, ) classifier = DomainClassifier(username="testuser") assert classifier.username == "testuser" assert classifier.settings_snapshot is None assert classifier.llm is None def test_init_with_settings_snapshot(self): """Test initialization with settings snapshot.""" from local_deep_research.domain_classifier.classifier import ( DomainClassifier, ) snapshot = {"llm.model": "test-model"} classifier = DomainClassifier( username="testuser", settings_snapshot=snapshot ) assert classifier.settings_snapshot == snapshot @patch("local_deep_research.domain_classifier.classifier.get_llm") def test_get_llm_creates_instance(self, mock_get_llm): """Test that _get_llm creates LLM instance.""" from local_deep_research.domain_classifier.classifier import ( DomainClassifier, ) mock_llm = Mock() mock_get_llm.return_value = mock_llm classifier = DomainClassifier(username="testuser") result = classifier._get_llm() assert result == mock_llm mock_get_llm.assert_called_once() @patch("local_deep_research.domain_classifier.classifier.get_llm") def test_get_llm_caches_instance(self, mock_get_llm): """Test that _get_llm caches LLM instance.""" from local_deep_research.domain_classifier.classifier import ( DomainClassifier, ) mock_llm = Mock() mock_get_llm.return_value = mock_llm classifier = DomainClassifier(username="testuser") result1 = classifier._get_llm() result2 = classifier._get_llm() assert result1 == result2 # Should only be called once due to caching assert mock_get_llm.call_count == 1 class TestDomainClassifierClassify: """Tests for classification functionality.""" @pytest.fixture def classifier(self): """Create a DomainClassifier instance for testing.""" from local_deep_research.domain_classifier.classifier import ( DomainClassifier, ) return DomainClassifier(username="testuser") @patch( "local_deep_research.domain_classifier.classifier.get_user_db_session" ) @patch("local_deep_research.domain_classifier.classifier.get_llm") def test_classify_domain_success( self, mock_get_llm, mock_get_db_session, classifier ): """Test successful domain classification.""" mock_llm = MagicMock() mock_response = Mock() mock_response.content = """ { "category": "Technology", "subcategory": "Software Development", "confidence": 0.9, "reasoning": "Domain is a software project" } """ mock_llm.invoke.return_value = mock_response mock_get_llm.return_value = mock_llm # Mock the database session context manager mock_session = MagicMock() mock_session.__enter__ = Mock(return_value=mock_session) mock_session.__exit__ = Mock(return_value=False) # No existing classification mock_session.query.return_value.filter_by.return_value.first.return_value = None mock_get_db_session.return_value = mock_session result = classifier.classify_domain("github.com") assert result is not None @patch( "local_deep_research.domain_classifier.classifier.get_user_db_session" ) @patch("local_deep_research.domain_classifier.classifier.get_llm") def test_classify_domain_handles_invalid_json( self, mock_get_llm, mock_get_db_session, classifier ): """Test handling of invalid JSON response from LLM.""" mock_llm = MagicMock() mock_response = Mock() mock_response.content = "Not valid JSON" mock_llm.invoke.return_value = mock_response mock_get_llm.return_value = mock_llm mock_session = MagicMock() mock_session.__enter__ = Mock(return_value=mock_session) mock_session.__exit__ = Mock(return_value=False) mock_session.query.return_value.filter_by.return_value.first.return_value = None mock_get_db_session.return_value = mock_session # Invalid JSON from the LLM raises ValueError inside the session # context, which the SUT's broad `except Exception` catches and # converts to a None return. (See classifier.py:225-265.) result = classifier.classify_domain("example.com") assert result is None @patch( "local_deep_research.domain_classifier.classifier.get_user_db_session" ) @patch("local_deep_research.domain_classifier.classifier.get_llm") def test_classify_domain_handles_llm_error( self, mock_get_llm, mock_get_db_session, classifier ): """Test handling of LLM errors during classification.""" mock_llm = MagicMock() mock_llm.invoke.side_effect = Exception("LLM error") mock_get_llm.return_value = mock_llm mock_session = MagicMock() mock_session.__enter__ = Mock(return_value=mock_session) mock_session.__exit__ = Mock(return_value=False) mock_session.query.return_value.filter_by.return_value.first.return_value = None mock_get_db_session.return_value = mock_session # Should handle error gracefully result = classifier.classify_domain("example.com") # Should return Unknown or None on error assert result is None or ( hasattr(result, "category") and "Unknown" in str(result.category) ) @patch( "local_deep_research.domain_classifier.classifier.get_user_db_session" ) def test_classify_domain_empty_domain( self, mock_get_db_session, classifier ): """Empty-string domain must not crash; the SUT routes through the LLM path (no cached row), and with no LLM mock configured the broad except in classify_domain converts the error to a None return.""" mock_session = MagicMock() mock_session.__enter__ = Mock(return_value=mock_session) mock_session.__exit__ = Mock(return_value=False) mock_session.query.return_value.filter_by.return_value.first.return_value = None mock_get_db_session.return_value = mock_session result = classifier.classify_domain("") assert result is None @patch( "local_deep_research.domain_classifier.classifier.get_user_db_session" ) def test_classify_domain_none_domain(self, mock_get_db_session, classifier): """Same path as the empty-string case: no cached row, no LLM mock, broad except → None return.""" mock_session = MagicMock() mock_session.__enter__ = Mock(return_value=mock_session) mock_session.__exit__ = Mock(return_value=False) mock_session.query.return_value.filter_by.return_value.first.return_value = None mock_get_db_session.return_value = mock_session result = classifier.classify_domain(None) assert result is None class TestClassifyAllDomains: """Tests for batch/all-domain classification functionality.""" def test_classify_all_domains_method_exists(self): """Test that classify_all_domains method exists.""" from local_deep_research.domain_classifier.classifier import ( DomainClassifier, ) classifier = DomainClassifier(username="testuser") assert hasattr(classifier, "classify_all_domains") assert callable(classifier.classify_all_domains) class TestCategoryValidation: """Tests for category validation.""" def test_all_subcategories_are_strings(self): """Test that all subcategories are strings.""" from local_deep_research.domain_classifier.classifier import ( DOMAIN_CATEGORIES, ) for category, subcategories in DOMAIN_CATEGORIES.items(): for subcat in subcategories: assert isinstance(subcat, str), ( f"Subcategory in {category} is not a string: {subcat}" ) def test_no_duplicate_subcategories_within_category(self): """Test that there are no duplicate subcategories within a category.""" from local_deep_research.domain_classifier.classifier import ( DOMAIN_CATEGORIES, ) for category, subcategories in DOMAIN_CATEGORIES.items(): unique_subcats = set(subcategories) assert len(unique_subcats) == len(subcategories), ( f"Duplicate subcategories in {category}" ) class TestUrlparseExceptionInClassifyAll: """Tests for urlparse exception handling in classify_all_domains (PR #2027). PR #2027 changed bare `except:` to `except Exception:` when parsing URLs to extract domains. """ def test_valid_url_extracts_domain(self): """Valid URL extracts domain correctly.""" from urllib.parse import urlparse url = "https://www.example.com/path" parsed = urlparse(url) domain = parsed.netloc.lower() if domain.startswith("www."): domain = domain[4:] assert domain == "example.com" def test_url_without_scheme_no_netloc(self): """URL without scheme has no netloc.""" from urllib.parse import urlparse url = "just-some-text" parsed = urlparse(url) domain = parsed.netloc.lower() assert domain == "" def test_empty_url_handled(self): """Empty URL string produces empty domain.""" from urllib.parse import urlparse url = "" parsed = urlparse(url) domain = parsed.netloc.lower() assert domain == ""