morphos / evals /tests /test_revision_y_dataset.py
Jose Salazar
Pruebas unitarias de los scripts de evals y target `make evals-unit`
af530a3
Raw
History Blame Contribute Delete
7.15 kB
"""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"]