from app.rag.chunking import chunk_text, tokenize from app.rag.ingest import parse_seance_html from app.rag.retriever import BM25Index HTML = """

5.1Cadrage#

La formule fondamentale V = V_T + C_N - D structure la méthode du coût. Le terrain ne se déprécie pas.

DéfinitionCoût de reproduction

Coût de construire une réplique exacte du bâtiment avec les mêmes matériaux.

5.2Le coût de remplacement

Le coût de remplacement est le coût d'un bâtiment d'utilité équivalente construit selon les normes actuelles. Il est généralement inférieur au coût de reproduction pour un bâtiment ancien.

""" def test_tokenize_normalises_accents_and_plurals() -> None: assert tokenize("Dépréciations physiques") == tokenize("dépréciation physique") assert "le" not in tokenize("le coût de la valeur") def test_chunk_text_splits_long_text() -> None: text = "\n\n".join(f"Paragraphe {i} " + "mot " * 200 for i in range(20)) chunks = chunk_text(text, module="M", section="S") assert len(chunks) > 1 assert all(c.module == "M" for c in chunks) def test_parse_seance_html_sections_and_katex() -> None: module, chunks = parse_seance_html(HTML, "IMM1033", "05") assert module.startswith("Séance 5") assert len(chunks) == 2 first = chunks[0] assert first.section.startswith("5.1") assert "$V = V_T + C_N - D$" in first.content assert "**Définition — Coût de reproduction**" in first.content assert first.url.endswith("/seance/05/#s-5-1") def test_bm25_search_ranks_relevant_section() -> None: _, chunks = parse_seance_html(HTML, "IMM1033", "05") class Row: def __init__(self, i, c): # noqa: ANN001 self.id, self.course_code, self.module, self.section = str(i), "IMM1033", c.module, c.section self.page, self.url, self.content, self.visibility, self.embedding = c.page, c.url, c.content, "students", None idx = BM25Index() idx.build([Row(i, c) for i, c in enumerate(chunks)]) hits = idx.search("coût de remplacement normes actuelles", top_k=2) assert hits and hits[0].section.startswith("5.2") assert idx.search("coût", courses={"IMM1003"}) == []