Saltar al contenido
Development

Revisé mi propia app de iOS con cinco agentes en paralelo: esto fue lo que encontraron

Por Victor Da Luz
iosswiftclaude-codecode-reviewdev-logdeep-cut-atlas

Esta app se renombró después a Deep Cut Atlas. Aquí se la llama “Discoverer” en todo el texto, porque así se llamaba el día que pasó esto.

Tenía unas cuantas miles de líneas de una app SwiftUI en main sin una segunda mirada encima. Es un proyecto individual, así que no hay compañero de equipo a quien mandarle un PR. Quería una revisión real, no un “se ve bien,” sino alguien realmente buscando el error que no puedo ver porque yo lo escribí. Así que dividí el código en cinco fragmentos y puse un agente distinto en cada uno, en paralelo, cada uno con instrucciones específicas.

El resultado fue más útil de lo esperado, y no por la razón esperada. Así fue como salió y qué fue lo que realmente encontró.

Por qué cinco, y por qué dividir por área

Un solo revisor sobre 3.600 líneas se vuelve superficial rápido, para cuando llega al archivo cuarenta ya está reconociendo patrones, no leyendo. Cinco revisores con ~700 líneas cada uno sí pueden leer cada línea de su fragmento y tenerla presente. Así que corté la app siguiendo las divisiones que ya existían: la capa de servicio de MusicKit, la capa de StoreKit y persistencia más los modelos de datos, las funciones de Discover y Playlist, el código de History, Settings y la estructura general de la app, y un quinto fragmento con las pruebas y la configuración de CI.

Cada uno recibió el mismo tipo de instrucciones: acá está el stack y las convenciones, acá están los archivos, hay que buscar errores de lógica, problemas de concurrencia de Swift 6, errores de observación de SwiftUI y violaciones de convenciones, y reportar solo problemas reales, citando la línea, además de señalar qué está genuinamente bien hecho. Ese último punto importa. Un revisor al que se le pide “encontrar problemas” los va a inventar. Un revisor al que se le pide “encontrar problemas reales y también decir qué está sólido” se mantiene honesto.

Lo que casi me salteé: verificar los hallazgos

Acá va la parte donde quiero ser honesto. Los agentes volvieron con una lista clara y segura de sí misma. La tentación es tomarla al pie de la letra y empezar a corregir. Yo no lo hice, releí el código fuente real de cada hallazgo de alta prioridad antes de creerlo. Eso fue lo que marcó la diferencia entre un hallazgo real y uno solo plausible.

Dos hallazgos sobrevivieron esa revisión y resultaron ser errores genuinos:

El primero estaba en la clave de coincidencia entre fuentes. La app hace coincidir el mismo álbum en tres lugares que no comparten IDs, la biblioteca, el catálogo de Apple Music y la lista de descartados, normalizando el título y el artista en una sola cadena de texto. El comentario de documentación prometía que la clave ignoraba los espacios en blanco. El código recortaba el título pero se olvidaba del artista:

// before
(title.trimmingCharacters(in: .whitespacesAndNewlines) + separator + artist)
    .lowercased()

Entonces un álbum descartado con un artista cuya cadena tenía un espacio al final dejaba de coincidir en silencio, y el lanzamiento que le había pedido a la app que ocultara volvía a aparecer. Poca probabilidad en la práctica, corrección de una sola línea, pero es exactamente el tipo de asimetría que se pasa por alto en el propio código, porque uno sabe lo que quiso decir.

El segundo era una condición de carrera en la pestaña History. El gesto de pull-to-refresh reinicia la lista a la página cero; “cargar más” agrega la siguiente página usando el conteo actual como desplazamiento. Ambas operaciones son asíncronas. Si una carga de “más” está en curso cuando llega un refresh, esa carga ya había capturado su desplazamiento antes de que el refresh reiniciara todo, entonces agrega una página obsoleta encima de la nueva, filas duplicadas o faltantes en un feed que ya se está moviendo debajo.

La protección que tenía evitaba dos cargas de “más” simultáneas, pero nunca contempló que un refresh compitiera con una carga de “más”.

El hallazgo más valioso no fue un error

El mejor hallazgo de todos fue sobre las pruebas, no sobre el código. El revisor señaló que el servicio simulado no tiene forma de lanzar errores, todos los métodos devuelven datos de muestra, ninguno puede fallar. Lo que significa que cada rama catch en los view models, cada camino de “mostrar el estado de error”, está completamente sin probar. El estado de la interfaz para una carga fallida es una parte central de la app y tenía cobertura cero. Una regresión que arruinara el manejo de errores pasaría por CI en verde sin problema.

Ese es el hallazgo que nunca habría anotado por mi cuenta, porque todas las pruebas pasaban y las pruebas que pasan se sienten como algo terminado. Pasaban porque solo ejercitaban el camino feliz. Verde no es lo mismo que cubierto.

Qué corregí ahora, y qué anoté para después

Corregí lo barato y seguro en una sola pasada: el error de recorte del artista (con una prueba que fija el comportamiento que prometía el comentario), y dos piezas de andamiaje muerto, un modelo de datos de relleno que seguía registrado en el contenedor de SwiftData y que más tarde se habría filtrado al esquema de CloudKit, y un archivo de relleno vacío cuyo propio encabezado decía que había que borrarlo apenas apareciera una utilidad real. Ya habían aparecido tres utilidades reales. La condición de carrera y la cobertura de pruebas del camino de error son trabajos más grandes, así que quedaron anotados como hallazgos para abordar con calma en vez de apurarlos junto con la limpieza.

La conclusión honesta: dividir el trabajo en paralelo hizo que la revisión fuera exhaustiva, pero el paso de verificación fue lo que la hizo confiable. Los agentes son buenos generando una lista plausible rápido. El criterio sobre qué elementos son reales, cuáles importan, y cuáles corregir ahora versus anotar para después, esa parte siguió siendo mía. Eso se siente como la división correcta del trabajo.

Lecturas relacionadas

Development

Configurando Claude Code para un proyecto de iOS

Escribiendo el CLAUDE.md de una nueva app de iOS antes de que exista una sola línea de Swift: la trampa de MusicKit en el simulador, las reglas de CloudKit para SwiftData, y por qué el enfoque de proyecto de aprendizaje vino primero.

Leer