"""Teste pentru app/embedding_cache.py -- cache persistent de vectori in SQLite. Acopera: round-trip serializare, blob corupt = miss, validare dimensiune la scriere, chunking >500 randuri, save partial esuat, purge orfane + modele vechi, purge NU ruleaza cand embed_fn esueaza. """ from __future__ import annotations import os import sqlite3 import tempfile from array import array import pytest from app.embedding_cache import ( EMB_DIM, blob_to_vector, load_cached_vectors, purge_stale, save_vectors, sync_corpus_vectors, text_hash, vector_to_blob, ) @pytest.fixture() def conn(monkeypatch): tmp = tempfile.mkdtemp() monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "embcache.db")) monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false") monkeypatch.setenv("AUTOPASS_EMBEDDINGS_ENABLED", "true") # A13d: anti-vacuos from app.config import get_settings get_settings.cache_clear() from app.db import init_db, get_connection init_db() c = get_connection() yield c c.close() get_settings.cache_clear() def _vec(seed: float = 1.0, dim: int = EMB_DIM) -> list[float]: return [seed + i * 0.001 for i in range(dim)] # --------------------------------------------------------------------------- # # Serializare # # --------------------------------------------------------------------------- # def test_roundtrip_vector_to_blob_blob_to_vector(): v = _vec(3.5) blob = vector_to_blob(v) assert isinstance(blob, bytes) assert len(blob) == EMB_DIM * 4 out = blob_to_vector(blob) assert isinstance(out, array) assert out == pytest.approx(v, rel=1e-6) def test_text_hash_deterministic_and_distinct(): assert text_hash("SCHIMB ULEI") == text_hash("SCHIMB ULEI") assert text_hash("SCHIMB ULEI") != text_hash("SCHIMB FILTRU") # --------------------------------------------------------------------------- # # load_cached_vectors # # --------------------------------------------------------------------------- # def test_load_cached_vectors_roundtrip(conn): h = text_hash("SCHIMB ULEI") save_vectors(conn, "model-a", [(h, _vec(1.0))]) out = load_cached_vectors(conn, "model-a", [h]) assert h in out assert out[h] == pytest.approx(_vec(1.0), rel=1e-6) def test_load_cached_vectors_miss_on_missing_hash(conn): out = load_cached_vectors(conn, "model-a", [text_hash("NECUNOSCUT")]) assert out == {} def test_load_cached_vectors_blob_corupt_e_miss(conn): h = text_hash("SCHIMB ULEI") conn.execute("BEGIN") conn.execute( "INSERT INTO embedding_cache (text_hash, model, vector) VALUES (?, ?, ?)", (h, "model-a", b"\x00\x01\x02"), # lungime gresita ) conn.execute("COMMIT") out = load_cached_vectors(conn, "model-a", [h]) assert h not in out def test_load_cached_vectors_scoped_pe_model(conn): h = text_hash("SCHIMB ULEI") save_vectors(conn, "model-a", [(h, _vec(1.0))]) out = load_cached_vectors(conn, "model-b", [h]) assert out == {} # --------------------------------------------------------------------------- # # save_vectors: validare dimensiune + chunking # # --------------------------------------------------------------------------- # def test_save_vectors_respinge_dimensiune_gresita(conn): h = text_hash("SCHIMB ULEI") save_vectors(conn, "model-a", [(h, [1.0, 2.0, 3.0])]) # nu e EMB_DIM out = load_cached_vectors(conn, "model-a", [h]) assert h not in out def test_save_vectors_chunking_peste_500_randuri(conn): items = [(text_hash(f"text-{i}"), _vec(float(i))) for i in range(1200)] save_vectors(conn, "model-a", items) hashes = [h for h, _ in items] out = load_cached_vectors(conn, "model-a", hashes) assert len(out) == 1200 for h, vec in items: assert out[h] == pytest.approx(vec, rel=1e-6) def test_save_vectors_insert_or_replace_idempotent(conn): h = text_hash("SCHIMB ULEI") save_vectors(conn, "model-a", [(h, _vec(1.0))]) save_vectors(conn, "model-a", [(h, _vec(2.0))]) # populare intrerupta -> completare row = conn.execute( "SELECT COUNT(*) AS n FROM embedding_cache WHERE model=? AND text_hash=?", ("model-a", h), ).fetchone() assert row["n"] == 1 out = load_cached_vectors(conn, "model-a", [h]) assert out[h] == pytest.approx(_vec(2.0), rel=1e-6) # --------------------------------------------------------------------------- # # purge_stale # # --------------------------------------------------------------------------- # def test_purge_stale_sterge_orfane_model_curent(conn): h1, h2 = text_hash("A"), text_hash("B") save_vectors(conn, "model-a", [(h1, _vec(1.0)), (h2, _vec(2.0))]) purge_stale(conn, "model-a", corpus_hashes=[h1]) # h2 nu mai e in corpus out = load_cached_vectors(conn, "model-a", [h1, h2]) assert h1 in out assert h2 not in out def test_purge_stale_sterge_modele_vechi(conn): h = text_hash("A") save_vectors(conn, "model-old", [(h, _vec(1.0))]) save_vectors(conn, "model-a", [(h, _vec(2.0))]) purge_stale(conn, "model-a", corpus_hashes=[h]) assert load_cached_vectors(conn, "model-old", [h]) == {} assert h in load_cached_vectors(conn, "model-a", [h]) # --------------------------------------------------------------------------- # # sync_corpus_vectors # # --------------------------------------------------------------------------- # def test_sync_corpus_vectors_cold_start(conn): calls = [] def embed_fn(texts): calls.append(list(texts)) return [_vec(float(i)) for i in range(len(texts))] texts = ["A", "B", "C"] vecs = sync_corpus_vectors(conn, "model-a", texts, embed_fn) assert len(vecs) == 3 assert calls == [texts] # toate 3 lipsesc din cache -> toate trimise la embed hashes = [text_hash(t) for t in texts] cached = load_cached_vectors(conn, "model-a", hashes) assert len(cached) == 3 def test_sync_corpus_vectors_warm_start_zero_embed_calls(conn): calls = [] def embed_fn(texts): calls.append(list(texts)) return [_vec(float(i)) for i in range(len(texts))] texts = ["A", "B", "C"] sync_corpus_vectors(conn, "model-a", texts, embed_fn) calls.clear() vecs2 = sync_corpus_vectors(conn, "model-a", texts, embed_fn) assert calls == [] # nimic nou de vectorizat assert len(vecs2) == 3 def test_sync_corpus_vectors_incremental_un_text_nou(conn): calls = [] def embed_fn(texts): calls.append(list(texts)) return [_vec(float(i)) for i in range(len(texts))] sync_corpus_vectors(conn, "model-a", ["A", "B"], embed_fn) calls.clear() sync_corpus_vectors(conn, "model-a", ["A", "B", "C"], embed_fn) assert calls == [["C"]] def test_sync_corpus_vectors_nu_purjeaza_singur_apelantul_decide(conn): """sync_corpus_vectors NU mai purjeaza -- e responsabilitatea apelantului, DUPA o indexare reusita (vezi ensure_embeddings_corpus). purge_stale ramane apelabil separat, explicit, de catre apelant.""" def embed_fn(texts): return [_vec(float(i)) for i in range(len(texts))] sync_corpus_vectors(conn, "model-a", ["A", "B"], embed_fn) sync_corpus_vectors(conn, "model-a", ["A"], embed_fn) # B disparut din corpus solicitat out = load_cached_vectors(conn, "model-a", [text_hash("A"), text_hash("B")]) assert text_hash("A") in out assert text_hash("B") in out # nepurjat automat -- sync_corpus_vectors nu mai face asta purge_stale(conn, "model-a", {text_hash("A")}) # apelantul purjeaza dupa indexare reusita out2 = load_cached_vectors(conn, "model-a", [text_hash("A"), text_hash("B")]) assert text_hash("A") in out2 assert text_hash("B") not in out2 def test_sync_corpus_vectors_nu_purjeaza_cand_embed_fn_esueaza(conn): def embed_fn_ok(texts): return [_vec(float(i)) for i in range(len(texts))] sync_corpus_vectors(conn, "model-a", ["A", "B"], embed_fn_ok) def embed_fn_broken(texts): raise RuntimeError("model indisponibil") with pytest.raises(RuntimeError): sync_corpus_vectors(conn, "model-a", ["A", "C"], embed_fn_broken) # "B" nu a fost purjat -- sync_corpus_vectors nu purjeaza niciodata singur. out = load_cached_vectors(conn, "model-a", [text_hash("A"), text_hash("B")]) assert text_hash("A") in out assert text_hash("B") in out class _LockingConn: """Wrapper peste o conexiune reala: simuleaza `database is locked` la BEGIN (exercita try/except-ul din save_vectors, nu il ocoleste).""" def __init__(self, real): self._real = real def execute(self, sql, *a, **kw): if sql.strip() == "BEGIN": raise sqlite3.OperationalError("database is locked") return self._real.execute(sql, *a, **kw) def executemany(self, *a, **kw): return self._real.executemany(*a, **kw) def __getattr__(self, name): return getattr(self._real, name) def test_sync_corpus_vectors_save_partial_esuat_continua_din_ram(conn): """Daca save_vectors esueaza (ex. DB locked la BEGIN), vectorii noi tot se intorc din RAM -- indexarea continua, doar persistarea in cache rateaza.""" locking = _LockingConn(conn) def embed_fn(texts): return [_vec(float(i)) for i in range(len(texts))] vecs = sync_corpus_vectors(locking, "model-a", ["A", "B"], embed_fn) assert len(vecs) == 2 assert vecs[0] == pytest.approx(_vec(0.0), rel=1e-6) # Nimic nu a fost persistat (BEGIN a esuat la fiecare chunk). out = load_cached_vectors(conn, "model-a", [text_hash("A"), text_hash("B")]) assert out == {}