Files
FabledScribe/tests/test_embeddings.py
T
bvandeusen b255a0f90e
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Successful in 54s
CI & Build / Build & push image (push) Successful in 1m14s
refactor: rename package fabledassistant -> scribe (code-only)
Renames src/fabledassistant -> src/scribe and all imports, plus the
default DB name and DB user/password (fabled -> scribe) in config +
compose. 952 refs / 154 files. Reverses the old 'internal name stays
fabledassistant' convention.

Code-only: live databases are still physically named 'fabledassistant'.
Deployed environments must set POSTGRES_DB / POSTGRES_USER (or rename the
DB) since the defaults now resolve to 'scribe'. Repo (FabledScribe), git
host (fabledsword), MCP (fabled-git) and the image name (fabledscribe)
are intentionally unchanged.

ruff check src/ clean locally; CI (typecheck + pytest) is the gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 15:48:35 -04:00

91 lines
3.2 KiB
Python

"""Tests for services.embeddings — fastembed backend.
We don't actually load the fastembed model in tests (heavy download).
Instead, mock _get_model to return a fake that produces deterministic vectors.
"""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from scribe.services.embeddings import (
_cosine_similarity, get_embedding,
)
# ─── cosine_similarity (pure logic) ──────────────────────────────────────────
def test_cosine_similarity_orthogonal_is_zero():
assert _cosine_similarity([1.0, 0.0], [0.0, 1.0]) == 0.0
def test_cosine_similarity_identical_is_one():
assert _cosine_similarity([1.0, 0.0], [1.0, 0.0]) == pytest.approx(1.0)
def test_cosine_similarity_opposite_is_negative_one():
assert _cosine_similarity([1.0, 0.0], [-1.0, 0.0]) == pytest.approx(-1.0)
def test_cosine_similarity_zero_length_safe():
"""Zero-magnitude vector must not divide-by-zero."""
assert _cosine_similarity([0.0, 0.0], [1.0, 0.0]) == 0.0
assert _cosine_similarity([1.0, 0.0], [0.0, 0.0]) == 0.0
def test_cosine_similarity_mismatched_dim_returns_zero():
"""Cross-migration safety: a 768-dim vs 384-dim comparison must not crash."""
assert _cosine_similarity([1.0] * 5, [1.0] * 3) == 0.0
def test_cosine_similarity_empty_inputs():
assert _cosine_similarity([], []) == 0.0
assert _cosine_similarity([], [1.0]) == 0.0
# ─── get_embedding (fastembed path) ──────────────────────────────────────────
@pytest.mark.asyncio
async def test_get_embedding_returns_list_of_floats():
"""get_embedding wraps the embedder; the result is a Python list of floats."""
fake_vec = MagicMock()
fake_vec.tolist.return_value = [0.1, 0.2, 0.3, 0.4]
fake_embedder = MagicMock()
fake_embedder.embed = MagicMock(return_value=iter([fake_vec]))
with patch(
"scribe.services.embeddings._get_model",
AsyncMock(return_value=fake_embedder),
):
out = await get_embedding("hello world")
assert out == [0.1, 0.2, 0.3, 0.4]
fake_embedder.embed.assert_called_once_with(["hello world"])
@pytest.mark.asyncio
async def test_get_embedding_ignores_legacy_model_param():
"""The `model` kwarg is preserved for backward-compat but should not affect
the fastembed call."""
fake_vec = MagicMock()
fake_vec.tolist.return_value = [0.0]
fake_embedder = MagicMock()
fake_embedder.embed = MagicMock(return_value=iter([fake_vec]))
with patch(
"scribe.services.embeddings._get_model",
AsyncMock(return_value=fake_embedder),
):
out = await get_embedding("x", model="ignored-model-name")
assert out == [0.0]
@pytest.mark.asyncio
async def test_get_embedding_propagates_model_load_failures():
"""If fastembed can't initialize, the error propagates — callers catch
and degrade to keyword search."""
with patch(
"scribe.services.embeddings._get_model",
AsyncMock(side_effect=RuntimeError("model load failed")),
):
with pytest.raises(RuntimeError, match="model load failed"):
await get_embedding("x")