morphos / evals /tests /test_generacion_incremental.py
Jose Salazar
Pruebas unitarias de los scripts de evals y target `make evals-unit`
af530a3
Raw
History Blame Contribute Delete
4.35 kB
"""La generación tiene que sobrevivir a que maten el proceso.
`generar_con_modelo` ya toleraba que fallara el modelo en un caso, pero escribía a disco sólo
al final: si el proceso moría, se perdía TODO lo generado. Medido el 2026-08-03 contra el Space
local, 70 minutos tirados. Estas pruebas fijan las dos mitades del arreglo —escribir en caliente
y poder reanudar— sin llamar a ningún modelo.
"""
from __future__ import annotations
import asyncio
import json
import pytest
import run_evals
from run_evals import cargar_predicciones, generar_con_modelo
CASOS = [
{"id": "caso-1", "paciente": {"especie": "canino"}, "valores": {"creat": 4.0},
"signos_clinicos": "", "esperado": {}},
{"id": "caso-2", "paciente": {"especie": "felino"}, "valores": {"creat": 3.0},
"signos_clinicos": "", "esperado": {}},
]
@pytest.fixture
def modelo_falso(monkeypatch):
"""Sustituye motor e inferencia; `registro` anota qué casos llegaron a generarse."""
from app.ai import service
from app.schemas import InterpretacionClinica, RespuestaInterpretacion
registro: list[str] = []
monkeypatch.setattr(run_evals, "_motor_determinista", lambda v, p: ([], []))
async def interpretar_falso(pet):
registro.append(pet.paciente.especie)
return RespuestaInterpretacion(
resultado=InterpretacionClinica(
interpretacion=f"Interpretación para {pet.paciente.especie}.",
idioma="es",
),
backend="falso",
modelo="falso",
)
monkeypatch.setattr(service, "interpretar", interpretar_falso)
return registro
def test_cada_prediccion_se_escribe_en_cuanto_llega(tmp_path, modelo_falso):
destino = tmp_path / "sub" / "preds.jsonl"
asyncio.run(generar_con_modelo(CASOS, "medgemma", incremental=destino))
guardadas = cargar_predicciones(destino)
assert set(guardadas) == {"caso-1", "caso-2"}
assert "canino" in guardadas["caso-1"]["interpretacion"]
def test_lo_generado_sobrevive_a_que_reviente_el_caso_siguiente(tmp_path, monkeypatch):
"""El escenario real: el proceso muere a mitad. Lo anterior tiene que estar en disco."""
from app.ai import service
from app.schemas import InterpretacionClinica, RespuestaInterpretacion
monkeypatch.setattr(run_evals, "_motor_determinista", lambda v, p: ([], []))
destino = tmp_path / "preds.jsonl"
async def interpretar_falso(pet):
if pet.paciente.especie == "felino":
raise KeyboardInterrupt # sustituto de un SIGKILL a mitad de corrida
return RespuestaInterpretacion(
resultado=InterpretacionClinica(interpretacion="Prosa canina.", idioma="es"),
backend="falso",
modelo="falso",
)
monkeypatch.setattr(service, "interpretar", interpretar_falso)
with pytest.raises(KeyboardInterrupt):
asyncio.run(generar_con_modelo(CASOS, "medgemma", incremental=destino))
# Antes de este arreglo el fichero ni existía: se escribía al terminar.
assert cargar_predicciones(destino).keys() == {"caso-1"}
def test_reanudar_no_regenera_lo_que_ya_esta_en_disco(tmp_path, modelo_falso):
destino = tmp_path / "preds.jsonl"
previas = {"caso-1": {"interpretacion": "Ya estaba.", "idioma": "es"}}
salidas = asyncio.run(
generar_con_modelo(CASOS, "medgemma", incremental=destino, ya_generados=previas)
)
assert salidas["caso-1"]["interpretacion"] == "Ya estaba."
assert modelo_falso == ["felino"], "sólo debía generarse el caso que faltaba"
# Y el que faltaba sí se anexa, sin duplicar el que ya estaba.
assert cargar_predicciones(destino).keys() == {"caso-2"}
def test_sin_incremental_no_se_escribe_nada(tmp_path, modelo_falso):
salidas = asyncio.run(generar_con_modelo(CASOS, "medgemma"))
assert set(salidas) == {"caso-1", "caso-2"}
assert list(tmp_path.iterdir()) == []
def test_cargar_predicciones_tolera_fichero_inexistente(tmp_path):
assert cargar_predicciones(tmp_path / "no-existe.jsonl") == {}
def test_cargar_predicciones_ignora_lineas_en_blanco(tmp_path):
ruta = tmp_path / "preds.jsonl"
ruta.write_text(
json.dumps({"id": "a", "interpretacion": {"interpretacion": "x"}}) + "\n\n",
encoding="utf-8",
)
assert set(cargar_predicciones(ruta)) == {"a"}