jurix-api / tests /test_eval /test_eval_harness.py
rahim237cm's picture
Claude Opus 5
test(eval): harnais de mesure de la recuperation
4f32045
Raw History Blame Contribute Delete
4.28 kB
"""
Tests du harnais d'evaluation.
Ce qui est verifie ici, c'est que le harnais REFUSE de produire des chiffres
faux : jeu non relu, corpus qui a bouge, recoupement lexical trop eleve. Un
harnais qui accepte tout produit des nombres, pas des mesures.
"""
import json
import numpy as np
import pytest
from scripts.eval.run_eval import (
_brute_force_rankings,
_load_eval_set,
_slice_and_renormalise,
)
def _write_set(path, items, snapshot=None):
path.write_text(json.dumps({
"version": 1,
"corpus_snapshot": snapshot or {"laws": 1, "articles": 1, "alembic_head": "abc"},
"items": items,
}, ensure_ascii=False), encoding="utf-8")
return path
def _item(**kwargs):
base = {
"question": "Quelle est la responsabilité des dirigeants ?",
"expected_article_id": 1,
"expected_law_id": 1,
"expected_article_number": "161",
"lexical_overlap": 0.1,
"reviewed": True,
"split": "dev",
}
base.update(kwargs)
return base
class TestGuards:
def test_missing_file_is_explicit(self, tmp_path):
with pytest.raises(SystemExit, match="absent"):
_load_eval_set(tmp_path / "nexiste.pas", "dev", False, True)
def test_unreviewed_items_are_refused(self, tmp_path):
path = _write_set(tmp_path / "set.json", [_item(reviewed=False)])
with pytest.raises(SystemExit, match="non relus"):
_load_eval_set(path, "dev", False, True)
def test_unreviewed_can_be_forced_explicitly(self, tmp_path):
path = _write_set(tmp_path / "set.json", [_item(reviewed=False)])
payload = _load_eval_set(path, "dev", True, True)
assert len(payload["items"]) == 1
@pytest.mark.asyncio
async def test_stale_corpus_snapshot_is_refused(self, tmp_path, db_session):
"""
Les identifiants d'article ne designent quelque chose que contre un etat
de corpus donne. Sans ce controle, les chiffres deviennent faux en
silence apres une reingestion.
"""
path = _write_set(
tmp_path / "set.json", [_item()],
snapshot={"laws": 999, "articles": 999999, "alembic_head": "perimee"},
)
with pytest.raises(SystemExit, match="corpus a change"):
_load_eval_set(path, "dev", False, False)
def test_split_selection(self, tmp_path):
path = _write_set(tmp_path / "set.json", [
_item(split="dev", expected_article_id=1),
_item(split="test", expected_article_id=2),
])
assert len(_load_eval_set(path, "dev", False, True)["items"]) == 1
assert len(_load_eval_set(path, "test", False, True)["items"]) == 1
assert len(_load_eval_set(path, "all", False, True)["items"]) == 2
class TestMatryoshkaSlicing:
def test_slicing_renormalises(self):
"""
Le prefixe d'un vecteur unitaire n'est PAS unitaire : sans
renormalisation, toute la comparaison cosinus est fausse.
"""
rng = np.random.default_rng(0)
matrix = rng.normal(size=(5, 3072)).astype(np.float32)
matrix /= np.linalg.norm(matrix, axis=1, keepdims=True)
sliced = _slice_and_renormalise(matrix, 768)
assert sliced.shape == (5, 768)
assert np.allclose(np.linalg.norm(sliced, axis=1), 1.0, atol=1e-5)
def test_zero_vector_does_not_divide_by_zero(self):
matrix = np.zeros((2, 3072), dtype=np.float32)
sliced = _slice_and_renormalise(matrix, 768)
assert np.isfinite(sliced).all()
class TestBruteForce:
def test_ranks_by_cosine_similarity(self):
article_ids = np.array([10, 20, 30])
corpus = np.array([
[1.0, 0.0],
[0.0, 1.0],
[0.7071, 0.7071],
], dtype=np.float32)
queries = np.array([[1.0, 0.0]], dtype=np.float32)
rankings = _brute_force_rankings(article_ids, corpus, queries, top_k=3)
assert rankings == [[10, 30, 20]]
def test_respects_top_k(self):
article_ids = np.array([1, 2, 3, 4])
corpus = np.eye(4, dtype=np.float32)
queries = np.array([[1.0, 0.0, 0.0, 0.0]], dtype=np.float32)
assert len(_brute_force_rankings(article_ids, corpus, queries, top_k=2)[0]) == 2