|
|
| """
|
| Tests unitarios del servidor de inferencia (pampar.inference).
|
|
|
| Qué prueban:
|
| - handle_infer: responde con infer_ok y texto no vacío
|
| - handle_infer: prompt vacío → error
|
| - handle_boot: responde con boot_ok y agents_md con secciones
|
| - Tipo desconocido → error
|
| - JSON inválido no rompe el loop (simulado)
|
| - Tokenizer y modelo son mockeados — no se carga el checkpoint real
|
| """
|
|
|
| from __future__ import annotations
|
|
|
| import json
|
| import sys
|
| from io import StringIO
|
| from pathlib import Path
|
| from types import SimpleNamespace
|
| from unittest.mock import MagicMock, patch
|
|
|
| import pytest
|
| import torch
|
|
|
| sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
| from pampar.inference import handle_boot, handle_infer
|
|
|
|
|
|
|
|
|
|
|
|
|
| @pytest.fixture()
|
| def mock_tokenizer():
|
| """Tokenizer falso que 'codifica' como lista de ints y 'decodifica' texto fijo."""
|
| tok = MagicMock()
|
| tok.Encode.return_value = [1, 2, 3, 4, 5]
|
| tok.Decode.return_value = "def hello():\n return 'world'"
|
| return tok
|
|
|
|
|
| @pytest.fixture()
|
| def mock_model():
|
| """Modelo falso cuyo generate devuelve un tensor con tokens extra."""
|
| model = MagicMock()
|
|
|
| fake_output = torch.tensor([[1, 2, 3, 4, 5, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]])
|
| model.generate.return_value = fake_output
|
| return model
|
|
|
|
|
| @pytest.fixture()
|
| def device():
|
| return torch.device("cpu")
|
|
|
|
|
|
|
|
|
|
|
|
|
| class TestHandleInfer:
|
| def test_responde_infer_ok(self, mock_model, mock_tokenizer, device, capsys):
|
| msg = {"type": "infer", "prompt": "### Problem:\nhello\n### Solution:\n", "max_tokens": 50}
|
| handle_infer(mock_model, mock_tokenizer, device, msg)
|
|
|
| captured = capsys.readouterr().out.strip()
|
| resp = json.loads(captured)
|
| assert resp["type"] == "infer_ok"
|
| assert "text" in resp
|
| assert len(resp["text"]) > 0
|
|
|
| def test_llama_generate_con_params(self, mock_model, mock_tokenizer, device, capsys):
|
| msg = {"type": "infer", "prompt": "abc", "max_tokens": 128, "temperature": 0.2}
|
| handle_infer(mock_model, mock_tokenizer, device, msg)
|
|
|
| call_kwargs = mock_model.generate.call_args
|
| assert call_kwargs[1]["max_tokens"] == 128 or call_kwargs.kwargs.get("max_tokens") == 128 or call_kwargs[0][1] == 128
|
|
|
| def test_prompt_vacio_devuelve_error(self, mock_model, mock_tokenizer, device, capsys):
|
| msg = {"type": "infer", "prompt": ""}
|
| handle_infer(mock_model, mock_tokenizer, device, msg)
|
|
|
| captured = capsys.readouterr().out.strip()
|
| resp = json.loads(captured)
|
| assert resp["type"] == "error"
|
| assert "prompt" in resp["message"].lower() or "vacío" in resp["message"].lower() or "vac" in resp["message"]
|
|
|
| def test_prompt_ausente_devuelve_error(self, mock_model, mock_tokenizer, device, capsys):
|
| msg = {"type": "infer"}
|
| handle_infer(mock_model, mock_tokenizer, device, msg)
|
|
|
| captured = capsys.readouterr().out.strip()
|
| resp = json.loads(captured)
|
| assert resp["type"] == "error"
|
|
|
| def test_tokenizer_encode_llamado(self, mock_model, mock_tokenizer, device, capsys):
|
| msg = {"type": "infer", "prompt": "test prompt"}
|
| handle_infer(mock_model, mock_tokenizer, device, msg)
|
| mock_tokenizer.Encode.assert_called_once_with("test prompt", out_type=int)
|
|
|
| def test_temperatura_default_valida(self, mock_model, mock_tokenizer, device, capsys):
|
| """Sin temperature en el msg usa el default 0.4 — no debe explotar."""
|
| msg = {"type": "infer", "prompt": "hello"}
|
| handle_infer(mock_model, mock_tokenizer, device, msg)
|
| captured = capsys.readouterr().out.strip()
|
| resp = json.loads(captured)
|
| assert resp["type"] == "infer_ok"
|
|
|
|
|
|
|
|
|
|
|
|
|
| class TestHandleBoot:
|
| def test_responde_boot_ok(self, tmp_path, capsys):
|
| """boot con workspace válido devuelve boot_ok con agents_md."""
|
|
|
| (tmp_path / "main.py").write_text("def hello():\n pass\n")
|
|
|
| msg = {"type": "boot", "workspace": str(tmp_path)}
|
| handle_boot(msg)
|
|
|
| captured = capsys.readouterr().out.strip()
|
| resp = json.loads(captured)
|
| assert resp["type"] == "boot_ok"
|
| assert "agents_md" in resp
|
| assert len(resp["agents_md"]) > 50
|
|
|
| def test_agents_md_tiene_secciones(self, tmp_path, capsys):
|
| """El AGENTS.md generado debe tener al menos Quick Reference y Boot protocol."""
|
| msg = {"type": "boot", "workspace": str(tmp_path)}
|
| handle_boot(msg)
|
|
|
| captured = capsys.readouterr().out.strip()
|
| resp = json.loads(captured)
|
| agents_md = resp["agents_md"]
|
|
|
| assert "## Quick Reference" in agents_md
|
| assert "## Boot protocol" in agents_md
|
|
|
| def test_workspace_inexistente_no_explota(self, capsys):
|
| """Un workspace inexistente debe devolver error, no excepción sin capturar."""
|
| msg = {"type": "boot", "workspace": "/ruta/que/no/existe/12345"}
|
|
|
| try:
|
| handle_boot(msg)
|
| captured = capsys.readouterr().out.strip()
|
| resp = json.loads(captured)
|
| assert resp["type"] in ("boot_ok", "error")
|
| except Exception as exc:
|
| pytest.fail(f"handle_boot lanzó excepción no capturada: {exc}")
|
|
|
| def test_workspace_vacio_no_explota(self, tmp_path, capsys):
|
| """Workspace sin archivos .py — debe funcionar igual."""
|
| msg = {"type": "boot", "workspace": str(tmp_path)}
|
| handle_boot(msg)
|
| captured = capsys.readouterr().out.strip()
|
| resp = json.loads(captured)
|
| assert resp["type"] == "boot_ok"
|
|
|