Files
semantica/tests/test_embedding_providers.py
T
Varun Sahni 5d554ec586 fix: cherry-pick recursion guard and doctor embedding checks from #1005, #1006
Consolidates the remaining #994 fixes into this PR so it can fully close
the issue, per maintainer request.

From #1005 (yzxcj797):
- EmbeddingGeneratorWithProvenance.__getattr__ self-recursion guard:
  accessing self._generator via attribute syntax re-entered __getattr__
  forever when _generator was absent (failed __init__, pickle/copy probes
  like __deepcopy__). Private-name lookups now raise AttributeError.
- 4 regression tests in TestMethodDispatchRecursion: default dispatch no
  longer self-recurses for generation/text, a user-registered custom
  method still takes precedence, and a bare provenance wrapper raises
  AttributeError instead of RecursionError.
  (The methods.py identity guards from #1005 are already present here.)

From #1006 (yzxcj797):
- doctor gains two embedding backend checks, "Embeddings
  (sentence-transformers)" and "Embeddings (fastembed)". Default is a
  cheap import+version check (uninstalled backend now reports fail with a
  pip hint instead of invisible). --deep-embeddings (or
  SEMANTICA_DOCTOR_DEEP_EMBEDDINGS=1) instantiates via TextEmbedder and
  embeds a probe, catching backends that import cleanly but cannot load
  (the #994 failure mode) via the hash-fallback-active signal.
  _DeepEmbeddingFailure marks post-import runtime/model-load failures so
  they get a remediation hint instead of a misleading pip-install hint.
- 7 tests in TestDoctorEmbeddings and TestDoctorEmbeddingHintsAndEnv.

Validation:
- tests/test_cli_commands.py: 237 passed (7 new)
- tests/test_embedding_providers.py: 9 passed (4 new)
- AST parse + import of all four modules OK
2026-08-20 08:17:46 +05:30

134 lines
5.4 KiB
Python

import sys
import os
import unittest
import numpy as np
# Add project root to path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from semantica.embeddings import TextEmbedder, EmbeddingGenerator
class TestEmbeddingProviders(unittest.TestCase):
def test_sentence_transformers_default(self):
print("\nTesting Sentence Transformers (Default)...")
embedder = TextEmbedder(method="sentence_transformers")
text = "This is a test sentence."
embedding = embedder.embed_text(text)
self.assertIsInstance(embedding, np.ndarray)
print(f"Embedding shape: {embedding.shape}")
# Default model is all-MiniLM-L6-v2 which is 384 dim
self.assertEqual(len(embedding), 384)
def test_sentence_transformers_custom_model(self):
print("\nTesting Sentence Transformers (Custom Model: all-mpnet-base-v2)...")
# all-mpnet-base-v2 produces 768 dim embeddings
try:
embedder = TextEmbedder(
method="sentence_transformers",
model_name="all-mpnet-base-v2"
)
text = "This is a test sentence."
embedding = embedder.embed_text(text)
self.assertIsInstance(embedding, np.ndarray)
print(f"Embedding shape: {embedding.shape}")
self.assertEqual(len(embedding), 768)
except Exception as e:
print(f"Skipping custom model test if download fails: {e}")
def test_fastembed_default(self):
print("\nTesting FastEmbed (Default)...")
try:
embedder = TextEmbedder(method="fastembed")
text = "This is a test sentence."
embedding = embedder.embed_text(text)
self.assertIsInstance(embedding, np.ndarray)
print(f"Embedding shape: {embedding.shape}")
# FastEmbed default is usually BAAI/bge-small-en-v1.5 (384 dim) or similar
self.assertTrue(len(embedding) > 0)
except ImportError:
print("FastEmbed not installed, skipping.")
def test_fastembed_custom_model(self):
print("\nTesting FastEmbed (Custom Model: BAAI/bge-small-en-v1.5)...")
try:
embedder = TextEmbedder(
method="fastembed",
model_name="BAAI/bge-small-en-v1.5"
)
text = "This is a test sentence."
embedding = embedder.embed_text(text)
self.assertIsInstance(embedding, np.ndarray)
print(f"Embedding shape: {embedding.shape}")
self.assertEqual(len(embedding), 384)
except ImportError:
print("FastEmbed not installed, skipping.")
except Exception as e:
print(f"FastEmbed custom model error: {e}")
def test_embedding_generator_config(self):
print("\nTesting EmbeddingGenerator with config...")
# Configure to use fastembed via EmbeddingGenerator
config = {
"text": {
"method": "fastembed",
"model_name": "BAAI/bge-small-en-v1.5"
}
}
generator = EmbeddingGenerator(config=config)
embeddings = generator.generate_embeddings(["Test text"], data_type="text")
self.assertEqual(embeddings.shape[1], 384)
print("EmbeddingGenerator config test passed.")
if __name__ == '__main__':
with open("test_results.txt", "w") as f:
runner = unittest.TextTestRunner(stream=f, verbosity=2)
unittest.main(testRunner=runner, exit=False)
class TestMethodDispatchRecursion(unittest.TestCase):
"""#994: built-in aliases are registered in the method registry onto the
wrapper functions themselves, so dispatching through the registry called a
wrapper back into itself with the same default method — a recursion storm
that surfaced as `maximum recursion depth exceeded` during model loading."""
def test_generate_embeddings_default_does_not_self_recurse(self):
from semantica.embeddings.methods import generate_embeddings
emb = generate_embeddings("recursion probe")
self.assertIsNotNone(emb)
def test_embed_text_default_does_not_self_recurse(self):
from semantica.embeddings.methods import embed_text
emb = embed_text("recursion probe", method="sentence_transformers")
self.assertIsNotNone(emb)
def test_custom_registered_method_still_wins(self):
from semantica.embeddings.methods import method_registry
calls = []
def spy(data, *a, **k):
calls.append(data)
return {"custom": True}
method_registry.register("generation", "my_custom_gen", spy)
try:
from semantica.embeddings.methods import generate_embeddings
out = generate_embeddings("payload", method="my_custom_gen")
self.assertEqual(out, {"custom": True})
self.assertEqual(calls, ["payload"])
finally:
method_registry.unregister("generation", "my_custom_gen")
def test_provenance_wrapper_missing_generator_raises_attribute_error(self):
# Partially-initialised wrappers (failed __init__, pickle/copy probes)
# must raise AttributeError, not RecursionError via __getattr__.
from semantica.embeddings.embeddings_provenance import (
EmbeddingGeneratorWithProvenance,
)
bare = EmbeddingGeneratorWithProvenance.__new__(
EmbeddingGeneratorWithProvenance
)
with self.assertRaises(AttributeError):
getattr(bare, "model")