File size: 4,331 Bytes
70e641d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
"""Regresión del troceo estructural (Tier 1 RAG).

Valida las invariantes del troceo sin depender del grupo pesado `rag`: las funciones de
troceo no importan pymupdf4llm/sentence-transformers a nivel de módulo, así que corren en
el entorno `dev` por defecto con el contador de tokens heurístico.
"""

from __future__ import annotations

from app.rag.ingest import (
    _MIN_TOKENS_FRAGMENTO,
    CHUNK_TOKENS,
    SOLAPE_TOKENS,
    _cargar_contador_tokens,
    _ensamblar_documento,
    _extraer_parrafos,
    _limpiar_titulo,
    _texto_contextualizado,
    _titulo_valido,
    _trocear_estructural,
)

contar = _cargar_contador_tokens("BAAI/bge-m3")  # cae a heurística por palabras


def _chunks_desde_paginas(paginas):
    doc = _ensamblar_documento(paginas)
    return _trocear_estructural(_extraer_parrafos(doc), contar)


def test_fragmentos_cruzan_saltos_de_pagina():
    paginas = [
        (10, "# Anemia Regenerativa\n\nLa anemia regenerativa cursa con reticulocitosis."),
        (11, "La respuesta medular continúa describiéndose aquí sin cambio de tema."),
    ]
    chunks = _chunks_desde_paginas(paginas)
    assert any(c.pmin < c.pmax for c in chunks), "ningún fragmento cruza el salto de página"


def test_capitulo_se_puebla_y_sin_markup():
    paginas = [(5, "# **Nonregenerative Anemia**\n\nAusencia de respuesta reticulocitaria clara.")]
    chunks = _chunks_desde_paginas(paginas)
    assert chunks and chunks[0].capitulo == "Nonregenerative Anemia"
    assert all("*" not in c.capitulo for c in chunks)


def test_se_filtra_ruido_de_marca_de_agua_y_numeros_sueltos():
    paginas = [(7, "vetbooks.ir\n7\n\nContenido clínico real sobre eritrocitos y anemia.")]
    chunks = _chunks_desde_paginas(paginas)
    assert chunks
    assert all("vetbooks" not in c.texto.lower() for c in chunks)
    assert all(c.texto.strip() != "7" for c in chunks)


def test_tamano_acotado_por_tokens():
    grande = "oración clínica de prueba. " * 400
    chunks = _chunks_desde_paginas([(1, f"# Capítulo\n\n{grande}")])
    assert len(chunks) > 1
    for c in chunks:
        assert contar(c.texto) <= CHUNK_TOKENS + SOLAPE_TOKENS + 5


def test_marcadores_de_pagina_no_se_almacenan():
    chunks = _chunks_desde_paginas([(3, "Texto normal de una página cualquiera.")])
    assert all("〔p" not in c.texto for c in chunks)


def test_no_fragmentos_triviales_cuando_hay_continuacion():
    # Un encabezado seguido de cuerpo del mismo capítulo debe fusionarse, no quedar suelto.
    cuerpo = ("Descripción amplia del hallazgo clínico con longitud más que suficiente para "
              "superar con holgura el umbral mínimo de tokens exigido a un fragmento del índice.")
    chunks = _chunks_desde_paginas([(2, f"# Hallazgos\n\n{cuerpo}")])
    assert len(chunks) == 1
    assert chunks[0].capitulo == "Hallazgos"
    assert chunks[0].texto.startswith("Hallazgos ")  # el encabezado se fusionó con el cuerpo
    assert contar(chunks[0].texto) >= _MIN_TOKENS_FRAGMENTO


def test_texto_contextualizado_antepone_contexto():
    assert _texto_contextualizado("Contexto clínico.", "Cuerpo.") == "Contexto clínico.\n\nCuerpo."
    # Sin contexto (fallo al generar) → texto original intacto.
    assert _texto_contextualizado("", "Cuerpo.") == "Cuerpo."


def test_titulo_valido_rechaza_basura_ocr():
    # Cadenas reales observadas en el volcado de recuperación.
    for basura in ["va — yy ~~e~~", "ge", "° ~~g~~ e ~~E~~ s", "nRBC 100 WBC", "yy", "e"]:
        assert _titulo_valido(_limpiar_titulo(basura)) is False, basura


def test_titulo_valido_acepta_capitulos_reales():
    for bueno in ["Nonregenerative Anemia", "9 Regenerative Anemia", "ERYTHROCYTES",
                  "Sodium to Potassium Ratio", "Hallazgos de Laboratorio"]:
        assert _titulo_valido(_limpiar_titulo(bueno)) is True, bueno


def test_encabezado_basura_no_contamina_capitulo():
    # Un encabezado basura entre capítulos válidos no debe sobrescribir el capítulo vigente.
    paginas = [(5, "# Anemia Regenerativa\n\nCuerpo clínico del capítulo válido y suficiente.\n\n"
                   "# nRBC 100 WBC\n\nMás cuerpo clínico que sigue tras la cabecera basura.")]
    chunks = _chunks_desde_paginas(paginas)
    assert all(c.capitulo == "Anemia Regenerativa" for c in chunks), [c.capitulo for c in chunks]