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
381 lines
13 KiB
Python
381 lines
13 KiB
Python
"""
|
|
Tests for embeddings/splitters/text_splitter_registry.py
|
|
|
|
Tests cover:
|
|
- get_text_splitter() function with various splitter types
|
|
- is_semantic_chunker_available() function
|
|
- VALID_SPLITTER_TYPES constant
|
|
"""
|
|
|
|
import os
|
|
|
|
import pytest
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
|
|
def _huggingface_hub_reachable() -> bool:
|
|
"""Return True when HuggingFace Hub is reachable (best-effort check)."""
|
|
try:
|
|
import httpx
|
|
|
|
resp = httpx.head(
|
|
"https://huggingface.co/sentence-transformers/all-mpnet-base-v2/resolve/main/config.json",
|
|
timeout=5,
|
|
follow_redirects=True,
|
|
)
|
|
return resp.status_code < 400
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
_skip_no_hf = pytest.mark.skipif(
|
|
not _huggingface_hub_reachable(),
|
|
reason="HuggingFace Hub is unreachable (network or auth)",
|
|
)
|
|
|
|
|
|
class TestValidSplitterTypes:
|
|
"""Tests for VALID_SPLITTER_TYPES constant."""
|
|
|
|
def test_valid_splitter_types_contains_recursive(self):
|
|
"""Test that recursive splitter is valid."""
|
|
from local_deep_research.embeddings.splitters.text_splitter_registry import (
|
|
VALID_SPLITTER_TYPES,
|
|
)
|
|
|
|
assert "recursive" in VALID_SPLITTER_TYPES
|
|
|
|
def test_valid_splitter_types_contains_token(self):
|
|
"""Test that token splitter is valid."""
|
|
from local_deep_research.embeddings.splitters.text_splitter_registry import (
|
|
VALID_SPLITTER_TYPES,
|
|
)
|
|
|
|
assert "token" in VALID_SPLITTER_TYPES
|
|
|
|
def test_valid_splitter_types_contains_sentence(self):
|
|
"""Test that sentence splitter is valid."""
|
|
from local_deep_research.embeddings.splitters.text_splitter_registry import (
|
|
VALID_SPLITTER_TYPES,
|
|
)
|
|
|
|
assert "sentence" in VALID_SPLITTER_TYPES
|
|
|
|
def test_valid_splitter_types_contains_semantic(self):
|
|
"""Test that semantic splitter is valid."""
|
|
from local_deep_research.embeddings.splitters.text_splitter_registry import (
|
|
VALID_SPLITTER_TYPES,
|
|
)
|
|
|
|
assert "semantic" in VALID_SPLITTER_TYPES
|
|
|
|
|
|
class TestGetTextSplitterRecursive:
|
|
"""Tests for get_text_splitter with recursive type."""
|
|
|
|
def test_get_text_splitter_recursive_default(self):
|
|
"""Test getting recursive splitter with defaults."""
|
|
from local_deep_research.embeddings.splitters.text_splitter_registry import (
|
|
get_text_splitter,
|
|
)
|
|
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
|
|
|
splitter = get_text_splitter(splitter_type="recursive")
|
|
|
|
assert isinstance(splitter, RecursiveCharacterTextSplitter)
|
|
|
|
def test_get_text_splitter_recursive_custom_chunk_size(self):
|
|
"""Test recursive splitter with custom chunk size."""
|
|
from local_deep_research.embeddings.splitters.text_splitter_registry import (
|
|
get_text_splitter,
|
|
)
|
|
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
|
|
|
splitter = get_text_splitter(
|
|
splitter_type="recursive",
|
|
chunk_size=500,
|
|
chunk_overlap=50,
|
|
)
|
|
|
|
assert isinstance(splitter, RecursiveCharacterTextSplitter)
|
|
assert splitter._chunk_size == 500
|
|
assert splitter._chunk_overlap == 50
|
|
|
|
def test_get_text_splitter_recursive_custom_separators(self):
|
|
"""Test recursive splitter with custom separators."""
|
|
from local_deep_research.embeddings.splitters.text_splitter_registry import (
|
|
get_text_splitter,
|
|
)
|
|
|
|
custom_separators = ["\n\n", "\n", " "]
|
|
|
|
splitter = get_text_splitter(
|
|
splitter_type="recursive",
|
|
text_separators=custom_separators,
|
|
)
|
|
|
|
assert splitter._separators == custom_separators
|
|
|
|
def test_get_text_splitter_recursive_default_separators(self):
|
|
"""Test recursive splitter uses default separators."""
|
|
from local_deep_research.embeddings.splitters.text_splitter_registry import (
|
|
get_text_splitter,
|
|
)
|
|
|
|
splitter = get_text_splitter(splitter_type="recursive")
|
|
|
|
# Default separators
|
|
assert "\n\n" in splitter._separators
|
|
assert "\n" in splitter._separators
|
|
|
|
|
|
class TestGetTextSplitterToken:
|
|
"""Tests for get_text_splitter with token type."""
|
|
|
|
def test_get_text_splitter_token(self):
|
|
"""Test getting token splitter."""
|
|
from local_deep_research.embeddings.splitters.text_splitter_registry import (
|
|
get_text_splitter,
|
|
)
|
|
from langchain_text_splitters import TokenTextSplitter
|
|
|
|
splitter = get_text_splitter(splitter_type="token")
|
|
|
|
assert isinstance(splitter, TokenTextSplitter)
|
|
|
|
def test_get_text_splitter_token_custom_params(self):
|
|
"""Test token splitter with custom parameters."""
|
|
from local_deep_research.embeddings.splitters.text_splitter_registry import (
|
|
get_text_splitter,
|
|
)
|
|
from langchain_text_splitters import TokenTextSplitter
|
|
|
|
splitter = get_text_splitter(
|
|
splitter_type="token",
|
|
chunk_size=256,
|
|
chunk_overlap=32,
|
|
)
|
|
|
|
assert isinstance(splitter, TokenTextSplitter)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
os.getenv("CI") is not None,
|
|
reason="Flaky in CI: sentence-transformers model download times out (>60s)",
|
|
)
|
|
class TestGetTextSplitterSentence:
|
|
"""Tests for get_text_splitter with sentence type."""
|
|
|
|
@_skip_no_hf
|
|
def test_get_text_splitter_sentence(self):
|
|
"""Test getting sentence splitter."""
|
|
from local_deep_research.embeddings.splitters.text_splitter_registry import (
|
|
get_text_splitter,
|
|
)
|
|
from langchain_text_splitters import (
|
|
SentenceTransformersTokenTextSplitter,
|
|
)
|
|
|
|
# Use small chunk size within model's token limit (384)
|
|
splitter = get_text_splitter(splitter_type="sentence", chunk_size=256)
|
|
|
|
assert isinstance(splitter, SentenceTransformersTokenTextSplitter)
|
|
|
|
@_skip_no_hf
|
|
def test_get_text_splitter_sentence_custom_params(self):
|
|
"""Test sentence splitter with custom parameters."""
|
|
from local_deep_research.embeddings.splitters.text_splitter_registry import (
|
|
get_text_splitter,
|
|
)
|
|
from langchain_text_splitters import (
|
|
SentenceTransformersTokenTextSplitter,
|
|
)
|
|
|
|
# Use chunk size within model's token limit (384)
|
|
splitter = get_text_splitter(
|
|
splitter_type="sentence",
|
|
chunk_size=200,
|
|
chunk_overlap=32,
|
|
)
|
|
|
|
assert isinstance(splitter, SentenceTransformersTokenTextSplitter)
|
|
|
|
|
|
class TestGetTextSplitterSemantic:
|
|
"""Tests for get_text_splitter with semantic type."""
|
|
|
|
def test_get_text_splitter_semantic_without_embeddings_raises(self):
|
|
"""Test that semantic splitter without embeddings raises ValueError."""
|
|
from local_deep_research.embeddings.splitters.text_splitter_registry import (
|
|
get_text_splitter,
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="requires 'embeddings' parameter"):
|
|
get_text_splitter(splitter_type="semantic")
|
|
|
|
def test_get_text_splitter_semantic_with_embeddings(self):
|
|
"""Test semantic splitter with embeddings."""
|
|
from local_deep_research.embeddings.splitters.text_splitter_registry import (
|
|
get_text_splitter,
|
|
)
|
|
|
|
mock_embeddings = MagicMock()
|
|
mock_chunker = MagicMock()
|
|
|
|
with patch(
|
|
"langchain_experimental.text_splitter.SemanticChunker",
|
|
return_value=mock_chunker,
|
|
) as mock_class:
|
|
splitter = get_text_splitter(
|
|
splitter_type="semantic",
|
|
embeddings=mock_embeddings,
|
|
)
|
|
|
|
assert splitter is mock_chunker
|
|
mock_class.assert_called_once()
|
|
call_kwargs = mock_class.call_args[1]
|
|
assert call_kwargs["embeddings"] is mock_embeddings
|
|
|
|
def test_get_text_splitter_semantic_with_threshold_type(self):
|
|
"""Test semantic splitter with custom threshold type."""
|
|
from local_deep_research.embeddings.splitters.text_splitter_registry import (
|
|
get_text_splitter,
|
|
)
|
|
|
|
mock_embeddings = MagicMock()
|
|
mock_chunker = MagicMock()
|
|
|
|
with patch(
|
|
"langchain_experimental.text_splitter.SemanticChunker",
|
|
return_value=mock_chunker,
|
|
) as mock_class:
|
|
get_text_splitter(
|
|
splitter_type="semantic",
|
|
embeddings=mock_embeddings,
|
|
breakpoint_threshold_type="standard_deviation",
|
|
)
|
|
|
|
call_kwargs = mock_class.call_args[1]
|
|
assert (
|
|
call_kwargs["breakpoint_threshold_type"] == "standard_deviation"
|
|
)
|
|
|
|
def test_get_text_splitter_semantic_with_threshold_amount(self):
|
|
"""Test semantic splitter with custom threshold amount."""
|
|
from local_deep_research.embeddings.splitters.text_splitter_registry import (
|
|
get_text_splitter,
|
|
)
|
|
|
|
mock_embeddings = MagicMock()
|
|
mock_chunker = MagicMock()
|
|
|
|
with patch(
|
|
"langchain_experimental.text_splitter.SemanticChunker",
|
|
return_value=mock_chunker,
|
|
) as mock_class:
|
|
get_text_splitter(
|
|
splitter_type="semantic",
|
|
embeddings=mock_embeddings,
|
|
breakpoint_threshold_amount=0.5,
|
|
)
|
|
|
|
call_kwargs = mock_class.call_args[1]
|
|
assert call_kwargs["breakpoint_threshold_amount"] == 0.5
|
|
|
|
def test_get_text_splitter_semantic_import_error(self):
|
|
"""Test semantic splitter raises ImportError if experimental not installed."""
|
|
from local_deep_research.embeddings.splitters.text_splitter_registry import (
|
|
get_text_splitter,
|
|
)
|
|
|
|
mock_embeddings = MagicMock()
|
|
|
|
with patch(
|
|
"langchain_experimental.text_splitter.SemanticChunker",
|
|
side_effect=ImportError("No module"),
|
|
):
|
|
with pytest.raises(ImportError, match="langchain-experimental"):
|
|
get_text_splitter(
|
|
splitter_type="semantic",
|
|
embeddings=mock_embeddings,
|
|
)
|
|
|
|
|
|
class TestGetTextSplitterInvalid:
|
|
"""Tests for get_text_splitter with invalid types."""
|
|
|
|
def test_get_text_splitter_invalid_type_raises(self):
|
|
"""Test that invalid splitter type raises ValueError."""
|
|
from local_deep_research.embeddings.splitters.text_splitter_registry import (
|
|
get_text_splitter,
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="Invalid splitter type"):
|
|
get_text_splitter(splitter_type="invalid_type")
|
|
|
|
def test_get_text_splitter_invalid_type_shows_valid_options(self):
|
|
"""Test that error message shows valid options."""
|
|
from local_deep_research.embeddings.splitters.text_splitter_registry import (
|
|
get_text_splitter,
|
|
)
|
|
|
|
with pytest.raises(ValueError) as exc_info:
|
|
get_text_splitter(splitter_type="unknown")
|
|
|
|
error_msg = str(exc_info.value)
|
|
assert "recursive" in error_msg
|
|
assert "token" in error_msg
|
|
assert "sentence" in error_msg
|
|
assert "semantic" in error_msg
|
|
|
|
|
|
class TestGetTextSplitterNormalization:
|
|
"""Tests for splitter type normalization."""
|
|
|
|
def test_get_text_splitter_normalizes_case(self):
|
|
"""Test that splitter type is case insensitive."""
|
|
from local_deep_research.embeddings.splitters.text_splitter_registry import (
|
|
get_text_splitter,
|
|
)
|
|
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
|
|
|
splitter = get_text_splitter(splitter_type="RECURSIVE")
|
|
assert isinstance(splitter, RecursiveCharacterTextSplitter)
|
|
|
|
splitter = get_text_splitter(splitter_type="Recursive")
|
|
assert isinstance(splitter, RecursiveCharacterTextSplitter)
|
|
|
|
def test_get_text_splitter_strips_whitespace(self):
|
|
"""Test that splitter type whitespace is stripped."""
|
|
from local_deep_research.embeddings.splitters.text_splitter_registry import (
|
|
get_text_splitter,
|
|
)
|
|
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
|
|
|
splitter = get_text_splitter(splitter_type=" recursive ")
|
|
assert isinstance(splitter, RecursiveCharacterTextSplitter)
|
|
|
|
|
|
class TestIsSemanticChunkerAvailable:
|
|
"""Tests for is_semantic_chunker_available function."""
|
|
|
|
def test_is_semantic_chunker_available_when_installed(self):
|
|
"""Test returns True when langchain_experimental is installed."""
|
|
from local_deep_research.embeddings.splitters.text_splitter_registry import (
|
|
is_semantic_chunker_available,
|
|
)
|
|
|
|
mock_spec = MagicMock()
|
|
|
|
with patch("importlib.util.find_spec", return_value=mock_spec):
|
|
assert is_semantic_chunker_available() is True
|
|
|
|
def test_is_semantic_chunker_available_when_not_installed(self):
|
|
"""Test returns False when langchain_experimental is not installed."""
|
|
from local_deep_research.embeddings.splitters.text_splitter_registry import (
|
|
is_semantic_chunker_available,
|
|
)
|
|
|
|
with patch("importlib.util.find_spec", return_value=None):
|
|
assert is_semantic_chunker_available() is False
|