|
|
| """
|
| Tests del runtime.Agente de PAMPAr-Coder v3.
|
|
|
| El Agente requiere sentencepiece + tokenizer .model para instanciarse,
|
| por eso estos tests evitan usar el constructor real y en su lugar:
|
| - Testean los métodos puramente lógicos usando mocks
|
| - Testean componentes integrados (sin modelo real) para el orquestador
|
|
|
| Cubren:
|
| - _parece_codigo() detecta código correctamente
|
| - _construir_prompt() incluye todos los bloques esperados
|
| - _procesar_acciones() parsea y ejecuta [LEER:], [EJECUTAR:], [TESTS:]
|
| - limpiar_historial() limpia el estado
|
| - stats() retorna dict con claves esperadas
|
| - aceptar_finetune/rechazar_finetune retornan strings
|
| - SYSTEM_PROMPT tiene las instrucciones esperadas
|
| """
|
|
|
| import sys
|
| import tempfile
|
| from pathlib import Path
|
| from unittest.mock import MagicMock, patch, PropertyMock
|
|
|
| import pytest
|
| import torch
|
|
|
|
|
| sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
| from pampar.runtime.agente import SYSTEM_PROMPT
|
|
|
|
|
|
|
|
|
|
|
|
|
| def _make_agente(tmp_path: Path) -> "Agente":
|
| """
|
| Crea un Agente real con modelo mini + tokenizer mockeado.
|
|
|
| Evita depender del tokenizer .model y del checkpoint pre-entrenado.
|
| El modelo se inicializa desde cero (pesos aleatorios).
|
| """
|
| from pampar.runtime.agente import Agente
|
| from pampar.coder.v3.config import ConfigV3
|
|
|
| config_mini = ConfigV3(
|
| vocab_size=256,
|
| dim=32,
|
| n_streams=4,
|
| n_levels=2,
|
| n_heads=2,
|
| n_kv_heads=1,
|
| ffn_mult=2.0,
|
| n_zonas=52,
|
| n_territorios=4,
|
| lateral_bottleneck=8,
|
| ventana_contexto=2,
|
| max_seq_len=32,
|
| dropout=0.0,
|
| use_checkpoint=False,
|
| )
|
|
|
|
|
| mock_tok = MagicMock()
|
| mock_tok.Encode.side_effect = lambda text: [1, 2, 3, 4, 5]
|
| mock_tok.Decode.side_effect = lambda ids: "respuesta mockeada"
|
| mock_tok.vocab_size.return_value = 256
|
| mock_tok.GetPieceSize.return_value = 256
|
| mock_tok.IdToPiece.side_effect = lambda i: str(i)
|
|
|
|
|
| with patch("pampar.runtime.agente.spm") as mock_spm:
|
| mock_spm.SentencePieceProcessor.return_value = mock_tok
|
|
|
| agente = Agente(
|
| checkpoint="no_existe_checkpoint.pt",
|
| tokenizer_path="no_existe_tokenizer.model",
|
| config=config_mini,
|
| workspace_root=str(tmp_path),
|
| memoria_dir=str(tmp_path / "memoria"),
|
| device="cpu",
|
| max_historial=5,
|
| )
|
|
|
| return agente
|
|
|
|
|
| @pytest.fixture
|
| def agente(tmp_path: Path):
|
| """Agente con modelo pequeño y tokenizer mockeado."""
|
| return _make_agente(tmp_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
| class TestSystemPrompt:
|
| def test_contiene_instruccion_leer(self):
|
| """El SYSTEM_PROMPT debe documentar la acción [LEER:]."""
|
| assert "[LEER:" in SYSTEM_PROMPT
|
|
|
| def test_contiene_instruccion_ejecutar(self):
|
| """El SYSTEM_PROMPT debe documentar la acción [EJECUTAR:]."""
|
| assert "[EJECUTAR:" in SYSTEM_PROMPT
|
|
|
| def test_contiene_instruccion_tests(self):
|
| """El SYSTEM_PROMPT debe documentar la acción [TESTS:]."""
|
| assert "[TESTS:" in SYSTEM_PROMPT
|
|
|
| def test_en_espanol(self):
|
| """El SYSTEM_PROMPT debe contener al menos una palabra española."""
|
| palabras_esp = ["archivos", "código", "Sos", "siempre", "acceso"]
|
| assert any(p in SYSTEM_PROMPT for p in palabras_esp)
|
|
|
|
|
|
|
|
|
|
|
|
|
| class TestLogicaSinModelo:
|
| def test_parece_codigo_con_def(self, agente):
|
| """Un texto con 'def' debe detectarse como código."""
|
| assert agente._parece_codigo("def calcular(x):\n return x * 2") is True
|
|
|
| def test_parece_codigo_con_class(self, agente):
|
| """Un texto con 'class' debe detectarse como código."""
|
| assert agente._parece_codigo("class Trainer:\n pass") is True
|
|
|
| def test_parece_codigo_con_import(self, agente):
|
| """Un texto con 'import' debe detectarse como código."""
|
| assert agente._parece_codigo("import torch\nimport numpy as np") is True
|
|
|
| def test_no_parece_codigo_texto_simple(self, agente):
|
| """Un texto conversacional corriente no debe detectarse como código."""
|
| assert agente._parece_codigo("¿Cómo estás? Cuéntame sobre Python.") is False
|
|
|
| def test_construir_prompt_incluye_system(self, agente):
|
| """El prompt construido debe incluir el SYSTEM_PROMPT."""
|
| prompt = agente._construir_prompt("hola", "")
|
| assert "PAMPAr" in prompt
|
|
|
| def test_construir_prompt_incluye_mensaje_usuario(self, agente):
|
| """El mensaje del usuario debe aparecer en el prompt."""
|
| prompt = agente._construir_prompt("¿qué es un decorador?", "")
|
| assert "¿qué es un decorador?" in prompt
|
|
|
| def test_construir_prompt_incluye_ctx_rag(self, agente):
|
| """El contexto RAG debe estar presente si se pasa."""
|
| ctx = "[MEMORIA RELEVANTE]\nEjemplo de código\n[/MEMORIA RELEVANTE]"
|
| prompt = agente._construir_prompt("explica esto", ctx)
|
| assert "[MEMORIA RELEVANTE]" in prompt
|
|
|
| def test_construir_prompt_incluye_historial(self, agente):
|
| """El historial previo debe aparecer en el prompt."""
|
| agente._historial = [
|
| {"role": "user", "text": "primer turno"},
|
| {"role": "assistant", "text": "primera respuesta"},
|
| ]
|
| prompt = agente._construir_prompt("segundo turno", "")
|
| assert "primer turno" in prompt
|
| assert "primera respuesta" in prompt
|
|
|
| def test_limpiar_historial(self, agente):
|
| """limpiar_historial() debe dejar el historial vacío."""
|
| agente._historial = [
|
| {"role": "user", "text": "algo"},
|
| {"role": "assistant", "text": "respuesta"},
|
| ]
|
| agente.limpiar_historial()
|
| assert agente._historial == []
|
|
|
| def test_stats_estructura(self, agente):
|
| """stats() debe retornar un dict con las claves esperadas."""
|
| s = agente.stats()
|
| for clave in ("modelo", "rag", "cola_finetune", "historial_turnos", "device"):
|
| assert clave in s, f"Falta clave '{clave}' en stats()"
|
|
|
| def test_stats_historial_turnos(self, agente):
|
| """stats()['historial_turnos'] debe contar pares user/assistant."""
|
| agente._historial = [
|
| {"role": "user", "text": "t1"},
|
| {"role": "assistant", "text": "r1"},
|
| {"role": "user", "text": "t2"},
|
| {"role": "assistant", "text": "r2"},
|
| ]
|
| assert agente.stats()["historial_turnos"] == 2
|
|
|
| def test_rechazar_finetune_retorna_string(self, agente):
|
| """rechazar_finetune() debe retornar un string no vacío."""
|
| msg = agente.rechazar_finetune()
|
| assert isinstance(msg, str)
|
| assert len(msg) > 10
|
|
|
| def test_aceptar_finetune_retorna_string(self, agente):
|
| """aceptar_finetune() debe retornar un string (éxito o error)."""
|
|
|
| msg = agente.aceptar_finetune()
|
| assert isinstance(msg, str)
|
| assert len(msg) > 10
|
|
|
| def test_describe_retorna_string(self, agente):
|
| """describe() debe delegar al modelo y retornar string."""
|
| desc = agente.describe()
|
| assert isinstance(desc, str)
|
| assert len(desc) > 0
|
|
|
|
|
|
|
|
|
|
|
|
|
| class TestProcesarAcciones:
|
| def test_sin_acciones_devuelve_original(self, agente):
|
| """Sin marcadores de acción, la respuesta se devuelve sin cambios."""
|
| respuesta = "Esto es una respuesta normal sin acciones."
|
| resultado = agente._procesar_acciones(respuesta)
|
| assert resultado == respuesta
|
|
|
| def test_accion_ejecutar(self, agente, tmp_path: Path):
|
| """[EJECUTAR: codigo] debe ejecutar el código e insertar el output."""
|
| respuesta = "[EJECUTAR:\nprint('desde accion')\n]"
|
| resultado = agente._procesar_acciones(respuesta)
|
|
|
| assert "[EJECUTAR:" not in resultado
|
|
|
| assert "desde accion" in resultado or "STDOUT" in resultado
|
|
|
| def test_accion_leer_archivo_valido(self, agente, tmp_path: Path):
|
| """[LEER: ruta] debe leer el archivo e insertar su contenido."""
|
|
|
| archivo = Path(agente.lector.root) / "leeme.py"
|
| archivo.write_text("# archivo de prueba\nx = 42\n", encoding="utf-8")
|
|
|
| respuesta = "[LEER: leeme.py]"
|
| resultado = agente._procesar_acciones(respuesta)
|
| assert "[LEER:" not in resultado
|
|
|
| assert "leeme.py" in resultado or "x = 42" in resultado
|
|
|
| def test_accion_leer_archivo_inexistente(self, agente):
|
| """[LEER: archivo_que_no_existe.py] debe insertar un mensaje de error."""
|
| respuesta = "[LEER: archivo_fantasma.py]"
|
| resultado = agente._procesar_acciones(respuesta)
|
| assert "[LEER:" not in resultado
|
|
|
| assert len(resultado) > 0
|
|
|
| def test_multiples_acciones(self, agente):
|
| """Múltiples acciones [EJECUTAR:] en la misma respuesta se procesan todas."""
|
| respuesta = (
|
| "Primero: [EJECUTAR:\nprint(1)\n]\n"
|
| "Luego: [EJECUTAR:\nprint(2)\n]"
|
| )
|
| resultado = agente._procesar_acciones(respuesta)
|
| assert "[EJECUTAR:" not in resultado
|
|
|