Files
LoreMind/brain/tests/test_vector_store.py
IETM_FIXE\ietm6 4d049274f9
Some checks failed
E2E Tests / e2e (push) Waiting to run
Tests unitaires / Web (Angular · vitest + couverture) (push) Successful in 26s
Build & Push Images / tests (push) Failing after 13s
Build & Push Images / build (brain) (push) Has been skipped
Build & Push Images / build (core) (push) Has been skipped
Build & Push Images / build (web) (push) Has been skipped
Build & Push Images / build-switcher (push) Has been skipped
Tests unitaires / Brain (Python · pytest + couverture) (push) Successful in 1m12s
Tests unitaires / Core (Java · mvn test + JaCoCo) (push) Failing after 1m28s
Refacto du code coté Python, java et coté angular afin de mieux séparer les responsabilité et d'avoir moins de répétitivité dans le code.
Mise en place de tests unitaires coté Python et Angular
Mise en place de la couverture de test directement dans le workflow : le programme ne build pas si jamais un test échoue
Passage en v0.16.2 en conséquence
2026-06-18 15:59:10 +02:00

120 lines
4.3 KiB
Python

"""Tests du stockage vectoriel fichier + recherche hybride (app.infrastructure.vector_store).
Le répertoire de stockage est redirigé vers un tmp_path et le cache mémoire est
vidé avant chaque test pour une isolation totale.
"""
from __future__ import annotations
import pytest
from app.infrastructure import vector_store
@pytest.fixture(autouse=True)
def isolated_store(tmp_path, monkeypatch):
monkeypatch.setattr(vector_store, "_STORE_DIR", tmp_path)
vector_store._CACHE.clear()
yield
vector_store._CACHE.clear()
# --- cosinus -------------------------------------------------------------------
def test_cosine_identical_is_one():
assert vector_store._cosine([1.0, 0.0], [2.0, 0.0]) == pytest.approx(1.0)
def test_cosine_orthogonal_is_zero():
assert vector_store._cosine([1.0, 0.0], [0.0, 1.0]) == 0.0
def test_cosine_mismatched_or_zero_is_zero():
assert vector_store._cosine([1.0], [1.0, 2.0]) == 0.0
assert vector_store._cosine([0.0, 0.0], [1.0, 1.0]) == 0.0
assert vector_store.cosine_similarity([], [1.0]) == 0.0 # alias public
# --- mots significatifs --------------------------------------------------------
def test_significant_words_filters_stopwords_and_short():
words = vector_store._significant_words("Le dragon DORT dans la caverne avec les gobelins")
assert "dragon" in words
assert "caverne" in words
assert "gobelins" in words
assert "les" not in words and "avec" not in words and "la" not in words
# --- save / exists / delete ----------------------------------------------------
def test_save_then_exists_and_delete():
vector_store.save("src1", ["chunk a"], [[1.0, 0.0]])
assert vector_store.exists("src1") is True
vector_store.delete("src1")
assert vector_store.exists("src1") is False
def test_save_rejects_mismatched_lengths():
with pytest.raises(ValueError):
vector_store.save("s", ["a", "b"], [[1.0]])
with pytest.raises(ValueError):
vector_store.save("s", ["a"], [[1.0]], pages=[1, 2])
def test_all_chunks_returns_text_and_page():
vector_store.save("s", ["t1", "t2"], [[1.0], [2.0]], pages=[3, 7])
chunks = vector_store.all_chunks("s")
assert chunks == [{"text": "t1", "page": 3}, {"text": "t2", "page": 7}]
# --- recherche -----------------------------------------------------------------
def test_search_ranks_by_cosine():
vector_store.save("s", ["proche", "loin"], [[1.0, 0.0], [0.0, 1.0]])
results = vector_store.search(["s"], [1.0, 0.0], top_k=2)
assert [r["text"] for r in results] == ["proche", "loin"]
assert results[0]["score"] > results[1]["score"]
def test_search_respects_top_k():
vector_store.save("s", ["a", "b", "c"], [[1.0], [0.9], [0.8]])
assert len(vector_store.search(["s"], [1.0], top_k=2)) == 2
def test_search_min_score_filters_out_weak_matches():
vector_store.save("s", ["proche", "orthogonal"], [[1.0, 0.0], [0.0, 1.0]])
results = vector_store.search(["s"], [1.0, 0.0], top_k=5, min_score=0.5)
assert [r["text"] for r in results] == ["proche"]
def test_search_lexical_bonus_promotes_exact_term_match():
# Deux extraits de cosinus IDENTIQUE : le bonus lexical départage celui qui
# contient le mot exact de la question.
vector_store.save(
"s",
["Strahd règne sur Barovia", "un texte neutre sans rapport"],
[[1.0, 0.0], [1.0, 0.0]],
)
results = vector_store.search(["s"], [1.0, 0.0], top_k=2, query_text="Strahd")
assert results[0]["text"] == "Strahd règne sur Barovia"
assert results[0]["score"] > results[1]["score"]
def test_search_includes_source_id_and_page():
vector_store.save("livre", ["extrait"], [[1.0]], pages=[42])
[res] = vector_store.search(["livre"], [1.0], top_k=1)
assert res["source_id"] == "livre"
assert res["page"] == 42
# --- résumés (analyse approfondie) ---------------------------------------------
def test_summaries_roundtrip_keyed_by_batch_tokens():
vector_store.save_summaries("s", 1000, [{"summary": "résumé", "vector": [1.0]}])
assert vector_store.load_summaries("s", 1000) == [{"summary": "résumé", "vector": [1.0]}]
# Taille de lot différente → invalidé (le découpage ne correspondrait plus).
assert vector_store.load_summaries("s", 2000) is None
def test_load_summaries_absent_returns_none():
assert vector_store.load_summaries("inconnu", 1000) is None