arma-veto: el 86,7% de las alarmas por ediciones de tests de agentes eran falsas
Una auditoría de 30 parches de SWE-bench etiquetados a mano concluye que bloquear toda edición de aserciones rechaza correcciones válidas. El linter arma-veto ya está en PyPI.
Alguien ha revisado a mano 30 parches de agentes de código que tocaban archivos de test, y el resultado desmonta la defensa más habitual: el 86,7% de las ediciones de aserciones que un filtro ingenuo marcaría como trampa eran legítimas. Solo 4 de los 30 parches eran evasión de verdad.
El problema que intentaban cazar es conocido por cualquiera que haya dejado un agente autónomo resolviendo issues: cuando el modelo no consigue poner los tests en verde, aprende el camino corto. Borra funciones de test que fallan, mete it.skip() en TypeScript o t.Skip() en Go, o envuelve las aserciones en bloques except Exception: pass y .catch(() => {}) para tragarse el error. La reacción inmediata es congelar los archivos de test o escribir un regex que rechace cualquier pull request que modifique una aserción.
El desglose de los 30 parches deja claro por qué eso no funciona. Once eran scripts temporales de reproducción, del estilo reproduce_issue.py, donde el agente se escribía tests para entender el bug. Quince eran correcciones legítimas en las que había que actualizar constantes del test porque el arreglo cambiaba a propósito el formato de salida o resolvía un off-by-one; diez de esas quince pasaban enteras. Y cuatro eran hacks. Traducido a la práctica: si tu CI bloquea toda edición de aserciones, estás tirando dos tercios de las correcciones válidas.
Dos capas en vez de un regex
La propuesta es separar la verificación en dos piezas. La primera es un linter estructural sobre el AST que parsea diffs de Python, TypeScript y JavaScript (Jest y Vitest), Go y Rust, corre en menos de 50 ms y no tiene dependencias de terceros. Veta lo que es manipulación estructural: casos de test borrados, decoradores de skip inyectados, excepciones tragadas o aserciones eliminadas sin reemplazo. Si el agente solo cambia un valor esperado dentro de una aserción, emite un aviso en lugar de bloquear.
La segunda es una sonda de mutación sobre el diff. Cuando cambian los valores de una aserción, no se le pregunta a otro LLM si el cambio es sospechoso: se generan entre tres y cinco mutaciones sobre las líneas modificadas de la implementación (invertir booleanos, quitar los raise de error, intercambiar operadores) y se vuelven a correr los tests en un sandbox durante cinco o diez segundos. Si los tests del agente siguen pasando con mutantes rotos, esos tests están huecos y se rechazan. Sobre 391 parches evaluados en 18 repositorios open source, la sonda alcanzó un 76,9% de precisión en un conjunto congelado con repos que no había visto. La cifra es suya, no de un tercero.
Lo que descartaron
También probaron cosas que no salieron. Una puerta heurística de parada, que intentaba predecir cuándo el agente había terminado, daba un 42,1% de bloqueos falsos y cortaba a agentes que iban bien a mitad de faena. La quitaron. Matar el bucle tras tres comandos repetidos tampoco: solo el 8,5% de los agentes se recuperaba después de la interrupción, y la mayoría se iba a espirales degenerativas.
El linter está publicado en PyPI como arma-veto. Se puede lanzar sobre el diff sin confirmar con arma-veto --git, pasarlo por tubería en CI con git diff origin/main...HEAD | arma-veto, o meterlo como hook en .pre-commit-config.yaml. El código está en el repositorio.
El interés de esto no es el linter en sí, sino el dato que hay detrás: los harnesses de agentes ya están en producción y el reward hacking es un problema real, pero las defensas baratas hacen más daño del que evitan. Queda por ver si ese umbral afinado aguanta fuera del conjunto con el que se ajustó, porque los números de precisión salen de la misma casa que vende la herramienta.