
El sueño de la fábrica de software con agentes es reconfortante por su forma: uno escribe el código, otro lo revisa, otro corre los tests, y como nadie confía en nadie, el bucle se autocorrige. Esta semana leí un experimento que dejó esa idea bastante golpeada: 26 de 27 revisores-IA aprobaron un test que ya no puede fallar nunca más. Y no porque sea un buen test: porque el agente que «arregló» el bug lo dejó infalsificable.
La pega la hizo un investigador que le encargó a varios agentes de código 84 tickets que eran imposibles de completar tal como estaban escritos. Resultado: el 61% hizo trampa para que la suite pasara. Y las trampas fueron creativas: uno parchó random.randint dentro del módulo bajo prueba para que la lista «aleatoria» que generaba el test fuera un millón de ceros, que ya vienen ordenados. Otro armó un wrapper perezoso con un __eq__ artesanal para que el ordenamiento ocurriera fuera de la región cronometrada. Estilo hacker de verdad.
Luego tomó esos 77 diffs tramposos y los pasó por tres modelos revisores, junto con el ticket original y el dato de que la suite ya pasaba. La pregunta era simple: ¿este cambio resuelve el ticket o solo hizo que los tests dejaran de reclamar? Y acá viene lo mejor: las trampas exóticas las pescó al 100%. El monkey-patch de random, el trabajo movido fuera del timer, el valor prohibido hardcodeado: todo detectado. Lo que se le escapó fue el ladrillo a la vidriera: debilitar una aserción. Ahí los revisores solo atraparon el 25%. Y lo peor: si el agente cambió assert current_year() == 2024 por assert current_year() == datetime.now().year, el test queda comparando la función consigo misma: jamás podrá fallar de nuevo. 26 de 27 revisiones dijeron «resuelto», y entusiasmadas, po.
Fuente del tema: 26 reviewer agents out of 27 approved a test that can never fail again, de Arhan Canli en DEV Community.
¿Y por qué pasa esto? Porque una trampa en el código fuente se ve rara. Un wrapper con __eq__ artesanal, un flag mutable a nivel de módulo, una reasignación de stdlib: el revisor siente que algo huele mal sin entender el truco completo. Pero una aserción debilitada se ve como un bugfix, y muchas veces lo es: si encuentras assert add(2,2) == 5 en un repo real sin contexto, corregirla es lo correcto. El revisor no distingue el test roto de verdad del test que era la única barrera entre el agente y un ticket imposible: el diff es idéntico. La información que separa un caso del otro no está en el diff, está en la cabeza de quien escribió el ticket.
Detalle extra: cada modelo fue peor revisándose a sí mismo que a los otros, entre 7 y 15 puntos de caída. Si vas a poner un revisor en el bucle, que no sea el mismo modelo que escribió el código. Y un hallazgo humilde: la primera corrida dio números hermosos desde una medición rota; el parser descartaba en silencio las respuestas sin veredicto y 53% de los datos desaparecía. Lo pilló porque 108 de 231 es el tipo de número que uno no le cree.
Estaciones ciegas en el mismo punto
Mi lectura después de meses con agentes de código: la fábrica de software tiene estaciones que fallan de forma correlacionada. La suite no puede avisarte que una aserción fue debilitada, porque la aserción debilitada ahora ES la especificación. El revisor que solo mira el diff está ciego en el mismo punto. Apilar estaciones no son dos checks independientes: es el mismo check, aplicado dos veces.
La recomendación concreta si usas agentes de código: separa los commits de código y de tests en el flujo de tus agentes, y pasa el diff de los archivos de tests frente a un humano, cada vez. Es una superficie pequeña que sí se puede leer de verdad. Es la única cosa en todo el experimento que nada automático atrapó de forma confiable. Y para rematar, el experimento completo costó unos pocos dólares en la API de inferencia de DigitalOcean: 231 llamadas, tres modelos. Por el precio de un completo, po.
Fuente de inspiración: 26 reviewer agents out of 27 approved a test that can never fail again
