"""Tests for src.cache.embedding_cache — disk persistence, hit/miss, namespace isolation.""" from __future__ import annotations from pathlib import Path from unittest.mock import MagicMock import pytest from src.cache.embedding_cache import _CachedEmbeddings, cached_embeddings class TestCachedEmbeddingsKey: """Keys must be deterministic and namespace-isolated.""" def test_key_deterministic(self, tmp_path: Path) -> None: mock = MagicMock(spec=["embed_documents", "embed_query"]) cache = _CachedEmbeddings(mock, namespace="ns1", cache_dir=tmp_path) k1 = cache._key("hello") k2 = cache._key("hello") assert k1 == k2 def test_key_namespace_sensitive(self, tmp_path: Path) -> None: mock = MagicMock(spec=["embed_documents", "embed_query"]) c1 = _CachedEmbeddings(mock, namespace="ns1", cache_dir=tmp_path) c2 = _CachedEmbeddings(mock, namespace="ns2", cache_dir=tmp_path) assert c1._key("hello") != c2._key("hello") def test_key_text_sensitive(self, tmp_path: Path) -> None: mock = MagicMock(spec=["embed_documents", "embed_query"]) cache = _CachedEmbeddings(mock, namespace="ns1", cache_dir=tmp_path) assert cache._key("hello") != cache._key("world") class TestCachedEmbeddingsReadWrite: """Round-trip a vector through the disk cache.""" def test_write_and_read(self, tmp_path: Path) -> None: mock = MagicMock(spec=["embed_documents", "embed_query"]) cache = _CachedEmbeddings(mock, namespace="ns1", cache_dir=tmp_path) vec = [0.1, 0.2, 0.3] cache._write("text", vec) loaded = cache._read("text") assert loaded == pytest.approx(vec) def test_read_miss(self, tmp_path: Path) -> None: mock = MagicMock(spec=["embed_documents", "embed_query"]) cache = _CachedEmbeddings(mock, namespace="ns1", cache_dir=tmp_path) assert cache._read("never_written") is None def test_file_created(self, tmp_path: Path) -> None: mock = MagicMock(spec=["embed_documents", "embed_query"]) cache = _CachedEmbeddings(mock, namespace="ns1", cache_dir=tmp_path) cache._write("x", [1.0]) assert cache._path("x").exists() def test_namespace_isolation_on_disk(self, tmp_path: Path) -> None: mock = MagicMock(spec=["embed_documents", "embed_query"]) c1 = _CachedEmbeddings(mock, namespace="ns1", cache_dir=tmp_path) c2 = _CachedEmbeddings(mock, namespace="ns2", cache_dir=tmp_path) c1._write("text", [1.0, 2.0]) assert c1._read("text") is not None assert c2._read("text") is None class TestCachedEmbeddingsQuery: """embed_query delegates to the underlying model on miss, returns cache on hit.""" def test_miss_calls_underlying(self, tmp_path: Path) -> None: mock = MagicMock(spec=["embed_documents", "embed_query"]) mock.embed_query.return_value = [0.5, 0.5] cache = _CachedEmbeddings(mock, namespace="ns", cache_dir=tmp_path) result = cache.embed_query("hello") assert result == [0.5, 0.5] mock.embed_query.assert_called_once_with("hello") def test_hit_skips_underlying(self, tmp_path: Path) -> None: mock = MagicMock(spec=["embed_documents", "embed_query"]) mock.embed_query.return_value = [0.9, 0.1] cache = _CachedEmbeddings(mock, namespace="ns", cache_dir=tmp_path) # First call — miss, writes cache cache.embed_query("hello") mock.embed_query.assert_called_once() # Second call — hit, should not call underlying again mock.reset_mock() result = cache.embed_query("hello") # float32 round-trip loses a little precision; use approx assert result == pytest.approx([0.9, 0.1]) mock.embed_query.assert_not_called() class TestCachedEmbeddingsDocuments: """embed_documents batches hits and misses correctly.""" def test_all_hits(self, tmp_path: Path) -> None: mock = MagicMock(spec=["embed_documents", "embed_query"]) cache = _CachedEmbeddings(mock, namespace="ns", cache_dir=tmp_path) # Pre-seed cache cache._write("a", [1.0]) cache._write("b", [2.0]) mock.reset_mock() result = cache.embed_documents(["a", "b"]) assert result == [[1.0], [2.0]] mock.embed_documents.assert_not_called() def test_all_misses(self, tmp_path: Path) -> None: mock = MagicMock(spec=["embed_documents", "embed_query"]) mock.embed_documents.return_value = [[1.0], [2.0]] cache = _CachedEmbeddings(mock, namespace="ns", cache_dir=tmp_path) result = cache.embed_documents(["a", "b"]) assert result == [[1.0], [2.0]] mock.embed_documents.assert_called_once_with(["a", "b"]) def test_mixed_hit_miss(self, tmp_path: Path) -> None: mock = MagicMock(spec=["embed_documents", "embed_query"]) mock.embed_documents.return_value = [[2.0]] cache = _CachedEmbeddings(mock, namespace="ns", cache_dir=tmp_path) cache._write("a", [1.0]) result = cache.embed_documents(["a", "b"]) assert result == [[1.0], [2.0]] mock.embed_documents.assert_called_once_with(["b"]) class TestCachedEmbeddingsFactory: """``cached_embeddings`` returns an ``Embeddings`` wrapper.""" def test_returns_cached_embeddings(self, tmp_path: Path) -> None: mock = MagicMock(spec=["embed_documents", "embed_query"]) result = cached_embeddings(mock, namespace="ns", cache_dir=tmp_path) assert isinstance(result, _CachedEmbeddings)