Si suma(2, 2) no devuelve 4, hay un bug: el test es un oráculo exacto. Un no funciona así. Entre la entrada y la salida hay decisiones de un modelo que no se pueden leer como código ni verificar con assert ==, y que cambian cuando cambias el , las herramientas o el modelo.
La salida no es renunciar al testing, sino separar el sistema en dos partes. Casi todo lo que rodea al modelo (el loop, el dispatcher, las políticas, los parsers, la persistencia) es determinista y se testea como cualquier software. Las decisiones del modelo se evalúan, con otras herramientas y otras expectativas. Veamos ambas mitades.
Tests y evals no son lo mismo
| Test | Eval | |
|---|---|---|
| Pregunta | ¿respeta un contrato conocido? | ¿el comportamiento es suficientemente bueno? |
| Resultado | pasa / falla | tasa, puntaje, distribución |
| Oráculo | exacto | aproximado: métricas, rúbricas, jueces, humanos |
| Qué cubre | el código | el sistema con el modelo |
| Costo | casi nulo | llamadas reales al modelo |
Los dos errores comunes son simétricos: testear todo con un modelo real y asserts exactos (tests lentos, caros y frágiles), o testear solo con mocks y no ver nunca el sistema con un modelo real (tests verdes, producto roto). Una suite de tests perfecta no prueba que el agente resuelva tareas; una buena no detecta que el dispatcher tiene un camino que salta la política.
El modelo falso
La técnica central es reemplazar el modelo por un backend guionado: un objeto que devuelve una secuencia fija de respuestas (incluidos tool calls) y registra lo que recibió. Así se provocan a propósito los casos que un modelo real produce rara vez: JSON roto, herramientas inexistentes, respuestas truncadas, pedidos peligrosos.
# Illustrative: script a model that tries to escape the workspace
backend = ScriptedBackend([
tool_call_response("c1", "write", '{"path": "../escape.txt"}'),
final_response("done"),
])
result = run_agent_turn(backend, registry, ctx, "session", "do it")
assert not (workspace.parent / "escape.txt").exists()
Esa frase es un buen criterio general: simula el modelo, no el protocolo. Si también falsificas la frontera HTTP, no detectarás el día en que el proveedor cambie un campo.
Tests que leen el código
Algunas garantías no son comportamientos, son invariantes de arquitectura: "toda tool call pasa por un único ". Un test de comportamiento prueba que hoy funciona; un test que lee el árbol de sintaxis prueba que nadie agregó un segundo camino.
# Excerpt, line-wrapped
call_sites = [
node
for node in ast.walk(tree)
if isinstance(node, ast.Call)
and isinstance(node.func, ast.Name)
and node.func.id == "dispatch_tool"
]
assert len(call_sites) == 1, (
"expected exactly one dispatch_tool(...) call site"
)
docket lleva la idea a una carpeta entera, tests/guards/: por ejemplo test_no_subprocess_in_core.py prohíbe subprocess dentro de core/, y test_function_span.py es un ratchet: ninguna función pasa de 150 líneas, y las que ya lo hacen, listadas en function_span_baseline.txt, solo pueden achicarse. El ratchet sirve para cualquier deuda técnica: no exige arreglar todo de golpe, prohíbe empeorar.
Contratos, golden files y costuras
- Contract tests. Cuando otro programa consume tu salida, el formato es un contrato. En docket,
tests/integration/test_harness_contract.pyregenera el JSON Schema dedocs/contracts/harness-v1/schema.jsondesde los modelos Pydantic y exige igualdad byte a byte. - Golden tests.
tests/golden/run.shejecuta la CLI real con unHOMEsembrado y binarios falsos, normaliza fechas e ids conscrub.pyy compara condiffcontra 18 archivos.golden. La regla del proyecto (AGENTS.md): nunca regenerar un golden para ocultar un cambio de comportamiento no intencional. - Costuras. Dos componentes testeados por separado pueden fallar juntos. En docket, un cambio hizo que el loop escribiera una denegación como texto y otro, en paralelo, la parseara con un regex; cada lado pasaba sus tests y juntos vaciaban los campos del payload. La corrección fue una sola función dueña del formato (
approval_unavailable_errorensrc/docket/core/agent_loop.py) y un test de ida y vuelta con el renderer y el parser reales (TestBlockedPayloadSurvivesTheDriverErrorRoundTrip).
Documentación que no puede mentir
Los números que un proyecto publica sobre sí mismo (tests, líneas, specs) se desactualizan en silencio. docket los calcula: scripts/metrics.py --check compara las cifras que cita CONTRIBUTING.md con el árbol y falla en CI si no coinciden (el paso se llama README drift guard en .github/workflows/ci.yml). El guard reemplazó a un script anterior que contaba rutas ya borradas y devolvía casi cero sin fallar, y tests/agent/truth/test_metrics_script.py existe para que eso no se repita.
Evals para agentes
Una eval de agente mide varias cosas a la vez:
- Resultado: ¿el artefacto final es correcto? Para código: compila, pasan los tests del proyecto y tests ocultos que el agente no vio.
- Trayectoria: ¿el camino fue razonable? ¿Tocó archivos prohibidos, intentó comandos peligrosos, modificó el test para que pasara?
- Costo: turnos, medidos, duración.
- Seguridad: casos adversariales y .
Agente A: grep → read → edit → pytest → final 5 pasos 12k tokens
Agente B: read ×14 → write test_login.py (borra test)
→ write auth.py → pytest → final 21 pasos 80k tokens
Una eval de resultado puntúa igual a los dos. Una de trayectoria descubre que B costó seis veces más y "aprobó" modificando el oráculo.
Esa eliminación es la lección: una eval que no puede fallar es peor que no tener eval, porque produce confianza falsa. Para evaluar modelos (held-out, baselines, ) ver Evaluar modelos.
Para CTOs
- Exige las dos mitades: tests deterministas en cada PR y evals con modelo real a un ritmo que puedas pagar (subconjunto de regresión en CI, suite completa nocturna o antes de cambiar de modelo).
- Pregunta por la trayectoria, no solo por la tasa de éxito.
- Cada incidente de producción debe convertirse en un caso nuevo del dataset.
- Desconfía de cualquier suite, eval o guard que nunca haya fallado: comprueba que puede ponerse en rojo.
Para llevar
- Separa lo determinista (se testea) de las decisiones del modelo (se evalúan).
- Simula el modelo con un backend guionado, pero conserva la frontera de protocolo real.
- Los invariantes de arquitectura, como un único chokepoint, se protegen leyendo el código con AST.
- Una eval de agente mira resultado, trayectoria y costo; un juez LLM necesita calibración.
- Una eval o un guard que no puede fallar produce confianza falsa.
Comprueba lo aprendido
¿Qué aporta un test AST sobre el chokepoint que un test de comportamiento no aporta?
El test de comportamiento prueba que el camino actual pasa por dispatch_tool. El test AST prueba que no existe otro camino en el código, así que falla si alguien agrega una llamada directa a un handler.
¿Por qué una eval de resultado final puede aprobar a un mal agente de código?
Porque solo mira el artefacto. Un agente que modifica o borra el test que fallaba produce un resultado "verde"; solo una eval de trayectoria o una revisión del diff lo detecta.
¿Por qué docket eliminó `docket eval` en lugar de dejarlo?
Porque dependía de un daemon ya eliminado y se saltaba en silencio en vez de fallar, así que parecía cobertura sin medir nada. Una eval que no puede fallar genera confianza falsa.