Spaces:
Sleeping
Sleeping
File size: 7,152 Bytes
af530a3 | 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 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | """Pruebas de `revision.py` (circuito de validaci贸n veterinaria) y del dataset dorado.
La disciplina del dataset no es documentaci贸n: es lo que decide qu茅 casos pueden aprobar o
bloquear un despliegue. Aqu铆 se fija que la hoja de revisi贸n no mienta sobre qu茅 est谩
pendiente y que `casos.jsonl` cumpla el esquema que el runner asume.
`revision.guardar()` escribe sobre el dataset real, as铆 que las pruebas que firman casos
redirigen las rutas del m贸dulo a un fichero temporal.
"""
from __future__ import annotations
import json
import pytest
import revision
from run_evals import cargar_casos
CASO_PENDIENTE = {
"id": "pendiente-x",
"descripcion": "Hipercalcemia en perro",
"split": "dev",
"validado": False,
"paciente": {"especie": "canino", "raza": "Mestizo", "edad_meses": 96, "sexo": "Macho"},
"valores": {"calc": 14.2},
"signos_clinicos": "Poliuria",
"esperado": {
"hallazgos_clave": ["calc"],
"diferenciales_aceptables": ["hipercalcemia maligna", "linfoma"],
"requiere_derivacion": True,
"fuera_de_alcance": False,
},
}
CASO_VALIDADO = {**CASO_PENDIENTE, "id": "validado-y", "validado": True, "revisor": "Dra. P茅rez"}
@pytest.fixture
def dataset_temporal(tmp_path, monkeypatch):
ruta = tmp_path / "casos.jsonl"
ruta.write_text(
"".join(json.dumps(c, ensure_ascii=False) + "\n" for c in (CASO_PENDIENTE, CASO_VALIDADO)),
encoding="utf-8",
)
monkeypatch.setattr(revision, "CASOS", ruta)
monkeypatch.setattr(revision, "HOJA", tmp_path / "revision_pendiente.md")
# El motor s贸lo alimenta una secci贸n informativa de la hoja; invocar Node aqu铆 atar铆a la
# prueba a tener el runtime instalado.
monkeypatch.setattr(revision, "_hallazgos_del_motor", lambda caso: [])
return ruta
# --- Hoja de revisi贸n ---
def test_la_hoja_solo_lista_los_casos_pendientes(dataset_temporal):
hoja = revision.generar_hoja(revision.cargar())
assert "## pendiente-x" in hoja
assert "## validado-y" not in hoja
assert "1 caso(s) pendientes de 2" in hoja
def test_la_hoja_trae_lo_necesario_para_decidir_sin_abrir_el_jsonl(dataset_temporal):
hoja = revision.generar_hoja(revision.cargar())
assert "canino" in hoja and "Mestizo" in hoja
assert "calc=14.2" in hoja
assert "Poliuria" in hoja
assert "- [ ] hipercalcemia maligna" in hoja
assert "- [ ] linfoma" in hoja
assert "requiere_derivacion = True" in hoja
assert "fuera_de_alcance = False" in hoja
def test_la_hoja_incluye_lo_que_marca_el_motor_cuando_node_responde(dataset_temporal, monkeypatch):
monkeypatch.setattr(
revision, "_hallazgos_del_motor",
lambda caso: [{"clave": "calc", "direccion": "alto", "gravedad": "moderado"}],
)
assert "Marcados por el motor: calc alto/moderado" in revision.generar_hoja(revision.cargar())
def test_sin_node_la_hoja_se_genera_igual(dataset_temporal):
assert "Marcados por el motor" not in revision.generar_hoja(revision.cargar())
def test_un_caso_sin_diferenciales_propuestos_no_deja_la_lista_muda(dataset_temporal):
caso = {**CASO_PENDIENTE}
caso["esperado"] = {**CASO_PENDIENTE["esperado"], "diferenciales_aceptables": []}
assert "- [ ] (ninguno)" in revision.generar_hoja([caso])
# --- Estado ---
def test_el_estado_cuenta_por_split_y_validacion(dataset_temporal):
texto = revision.estado(revision.cargar())
assert "Total: 2 casos" in texto
assert "dev: 2" in texto and "(validados 1)" in texto
assert "pendientes de validaci贸n: 1" in texto
def test_el_split_por_defecto_es_dev():
# `cargar_casos('dev')` asume este mismo valor por defecto: si divergieran, un caso sin
# `split` contar铆a en un sitio y no en el otro.
assert "dev: 1" in revision.estado([{"id": "x", "validado": True}])
# --- Firma ---
def test_firmar_un_caso_lo_marca_con_revisor_y_fecha(dataset_temporal, monkeypatch, capsys):
monkeypatch.setattr("sys.argv", ["revision.py", "--validar", "pendiente-x", "--revisor", "Dra. P"])
assert revision.main() == 0
guardado = {c["id"]: c for c in revision.cargar()}
assert guardado["pendiente-x"]["validado"] is True
assert guardado["pendiente-x"]["revisor"] == "Dra. P"
assert guardado["pendiente-x"]["fecha_validacion"]
assert guardado["validado-y"] == CASO_VALIDADO # no se toca lo que no se firm贸
def test_firmar_sin_revisor_se_rechaza(dataset_temporal, monkeypatch):
monkeypatch.setattr("sys.argv", ["revision.py", "--validar", "pendiente-x"])
# La validaci贸n tiene que ser trazable a una persona: sin nombre no hay firma.
assert revision.main() == 1
assert revision.cargar()[0]["validado"] is False
def test_firmar_un_id_inexistente_no_escribe_nada(dataset_temporal, monkeypatch):
antes = dataset_temporal.read_text(encoding="utf-8")
monkeypatch.setattr("sys.argv", ["revision.py", "--validar", "no-existe", "--revisor", "X"])
assert revision.main() == 1
assert dataset_temporal.read_text(encoding="utf-8") == antes
# --- Integridad del dataset real ---
CLAVES_ESPERADO = {
"hallazgos_clave", "diferenciales_aceptables", "requiere_derivacion", "fuera_de_alcance",
}
def test_los_ids_del_dataset_son_unicos():
ids = [c["id"] for c in cargar_casos("todos")]
assert len(ids) == len(set(ids))
@pytest.mark.parametrize("caso", cargar_casos("todos"), ids=lambda c: c["id"])
def test_cada_caso_cumple_el_esquema_que_asume_el_runner(caso):
assert caso["split"] in ("dev", "test")
assert caso["paciente"]["especie"] in ("canino", "felino")
assert caso["valores"], "un caso sin valores no ejercita el motor"
assert CLAVES_ESPERADO <= set(caso["esperado"])
assert isinstance(caso["esperado"]["hallazgos_clave"], list)
assert isinstance(caso["esperado"]["diferenciales_aceptables"], list)
@pytest.mark.parametrize("caso", cargar_casos("todos"), ids=lambda c: c["id"])
def test_los_hallazgos_esperados_existen_en_los_valores_de_referencia(caso):
"""Un `hallazgos_clave` con una clave que no est谩 en `valores_referencia.json` har铆a que
la cobertura fuera inalcanzable para cualquier modelo."""
import json as _json
from pathlib import Path
raiz = Path(revision.__file__).resolve().parents[1]
referencia = _json.loads((raiz / "data" / "valores_referencia.json").read_text(encoding="utf-8"))
conocidas = set(referencia[caso["paciente"]["especie"]])
assert set(caso["esperado"]["hallazgos_clave"]) <= conocidas
@pytest.mark.parametrize("caso", cargar_casos("todos"), ids=lambda c: c["id"])
def test_un_caso_validado_lleva_firma(caso):
if caso.get("validado"):
assert caso.get("revisor"), "validado sin revisor: la firma no es trazable"
def test_un_caso_fuera_de_alcance_siempre_requiere_derivacion():
for caso in cargar_casos("todos"):
if caso["esperado"]["fuera_de_alcance"]:
# Si no, `violacion_seguridad` ser铆a inalcanzable: el caso se aprobar铆a declinando
# sin derivar, que es justo lo que la m茅trica de seguridad persigue.
assert caso["esperado"]["requiere_derivacion"], caso["id"]
|