"""High-value edge case tests for embeddings/splitters/text_splitter_registry.py. Covers gaps: VALID_SPLITTER_TYPES structure, None vs empty separators, length_function verification, normalization across all types, semantic kwarg edge cases, and is_semantic_chunker_available return type. """ import pytest from unittest.mock import patch, MagicMock from local_deep_research.constants import DEFAULT_LOCAL_SEARCH_TEXT_SEPARATORS from local_deep_research.embeddings.splitters.text_splitter_registry import ( get_text_splitter, is_semantic_chunker_available, VALID_SPLITTER_TYPES, ) class TestValidSplitterTypes: """Validate the VALID_SPLITTER_TYPES constant.""" def test_is_a_list(self): """VALID_SPLITTER_TYPES is a list (not set or tuple).""" assert isinstance(VALID_SPLITTER_TYPES, list) def test_has_exactly_four_entries(self): """There are exactly 4 valid splitter types.""" assert len(VALID_SPLITTER_TYPES) == 4 class TestRecursiveSplitterEdgeCases: """Edge cases for recursive splitter type.""" def test_default_separators_exact_order(self): """Default separators are in the exact expected order.""" splitter = get_text_splitter("recursive") assert splitter._separators == DEFAULT_LOCAL_SEARCH_TEXT_SEPARATORS def test_text_separators_none_uses_defaults(self): """Explicitly passing text_separators=None uses defaults.""" splitter = get_text_splitter("recursive", text_separators=None) assert splitter._separators == DEFAULT_LOCAL_SEARCH_TEXT_SEPARATORS def test_empty_list_separators_passes_through(self): """Empty list text_separators=[] is NOT replaced with defaults. The code checks `if text_separators is None` not `if not text_separators`, so an empty list should pass through. If it doesn't, the code replaces it with defaults, which is also a valid design choice we verify here. """ splitter = get_text_splitter("recursive", text_separators=[]) # Empty list is falsy but not None - test what the code actually does # The code has `if text_separators is None:` so [] should pass through # But RecursiveCharacterTextSplitter may add default separators internally assert isinstance(splitter._separators, list) def test_uses_len_as_length_function(self): """RecursiveCharacterTextSplitter uses built-in len.""" splitter = get_text_splitter("recursive") assert splitter._length_function is len class TestNormalizationAcrossTypes: """Verify normalization (strip+lower) works for all splitter types.""" def test_token_type_uppercase(self): """'TOKEN' normalizes to token splitter.""" splitter = get_text_splitter("TOKEN") from langchain_text_splitters import TokenTextSplitter assert isinstance(splitter, TokenTextSplitter) def test_sentence_mixed_case_whitespace(self): """' Sentence ' normalizes to sentence splitter. Note: SentenceTransformersTokenTextSplitter requires downloading a model, so we patch the constructor to avoid that overhead. """ from unittest.mock import patch as _patch # ``get_text_splitter`` imports this class lazily (function-local) so # the heavy langchain_text_splitters/torch stack stays off the # app-startup path — patch it at its source module, which is where # the function-local ``from ... import`` resolves it. with _patch( "langchain_text_splitters.sentence_transformers.SentenceTransformersTokenTextSplitter" ) as mock_cls: mock_cls.return_value = MagicMock() get_text_splitter(" Sentence ") mock_cls.assert_called_once() def test_invalid_after_normalization(self): """' BOGUS ' raises ValueError after normalization.""" with pytest.raises(ValueError, match="bogus"): get_text_splitter(" BOGUS ") class TestSemanticSplitterEdgeCases: """Edge cases for semantic splitter type.""" def test_without_embeddings_raises(self): """Semantic splitter without embeddings raises ValueError.""" with pytest.raises(ValueError, match="embeddings"): get_text_splitter("semantic") @patch( "local_deep_research.embeddings.splitters.text_splitter_registry.SemanticChunker", create=True, ) def test_breakpoint_threshold_amount_zero_forwarded(self, mock_chunker_cls): """breakpoint_threshold_amount=0 is forwarded (not treated as None).""" mock_embeddings = MagicMock() with patch( "local_deep_research.embeddings.splitters.text_splitter_registry.SemanticChunker", mock_chunker_cls, ): try: get_text_splitter( "semantic", embeddings=mock_embeddings, breakpoint_threshold_amount=0, ) except Exception: pass # SemanticChunker import may fail # Verify that if the chunker was called, 0 was passed if mock_chunker_cls.called: call_kwargs = mock_chunker_cls.call_args[1] assert "breakpoint_threshold_amount" in call_kwargs assert call_kwargs["breakpoint_threshold_amount"] == 0 class TestIsSemanticChunkerAvailable: """Test is_semantic_chunker_available.""" def test_returns_bool(self): """Return type is exactly bool, not a truthy ModuleSpec.""" result = is_semantic_chunker_available() assert isinstance(result, bool) @patch("importlib.util.find_spec", return_value=None) def test_returns_false_when_not_installed(self, mock_find): result = is_semantic_chunker_available() assert result is False @patch("importlib.util.find_spec", return_value=MagicMock()) def test_returns_true_when_installed(self, mock_find): result = is_semantic_chunker_available() assert result is True