Spaces:
Sleeping
Sleeping
| """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": {}}, | |
| ] | |
| 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"} | |