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.
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
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
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
This commit is contained in:
119
brain/tests/test_vector_store.py
Normal file
119
brain/tests/test_vector_store.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user