DEV Community

Eduardo Fuentes
Eduardo Fuentes

Posted on

FinanceHub #2: Cuando el código compila pero igual está mal

Introducción

En el artículo anterior conté cómo decidimos migrar FinanceHub de una plataforma no-code a un backend propio, y que la parte difícil no fue escribir código: fue decidir cómo construirlo.

Dije que antes de pedirle cualquier tarea a un agente de IA, definimos la arquitectura, el modelo de datos, el contrato de API y un plan por fases.

Esta es la parte de cómo eso se sostuvo en la práctica, y lo que encontramos cuando lo pusimos a prueba de verdad.


Empezar por la spec, no por el código

La regla de fondo fue simple de enunciar y difícil de respetar bajo presión: el contrato de API (openapi.yml) y el schema de base de datos se trataron como entradas fijas, no como algo que se pudiera improvisar al toparse con un gap.

En concreto:

  • Si un campo no está en el contrato, no existe en el DTO.
  • Protocolo contract-first: cualquier cambio de forma en un endpoint actualiza openapi.yml primero, se avisa explícitamente, y recién después se escribe código. Nunca al revés.
  • El plan completo se escribió por adelantado, descompuesto en fases y tareas numeradas, cada una acotada a aproximadamente una entidad de dominio y su CRUD, con su propio paso de verificación decidido antes de empezar.
  • Reglas vivas en archivos propios (.claude/rules/*.md) para todo lo que alguien nuevo en el código se equivocaría si no se le dice explícitamente: qué campos son calculados por triggers y nunca deben aceptarse en un request, la forma exacta de la paginación (no la que parece obvia a primera vista), el envelope de error estándar.

Nada de esto es exótico. Es básicamente spec-driven development, aplicado de forma intuitiva antes de saberle poner nombre. Lo que sí cambió las reglas del juego fue quién ejecutaba el plan: un agente de IA, tarea por tarea, contra ese contrato fijo.


"Compila y los tests pasan" no fue nunca la vara de medida

Con un agente escribiendo la mayoría del código, la pregunta dejó de ser "¿funciona?" y pasó a ser "¿cómo lo sé?".

La respuesta fue una verificación de 6 pasos por tarea, siempre en el mismo orden:

  1. Compilar limpio.
  2. Correr la suite de tests contra Postgres — el stack usa citext, row-level security y el schema auth de Supabase, así que nunca corrió contra un H2 en memoria.
  3. Levantar la app completa.
  4. Obtener un JWT emitido por Supabase, nunca un stub sintético.
  5. Probar a mano el endpoint: happy path, validación, ownership, 401/404.
  6. Limpiar los datos de prueba.

Ese paso 4 en particular parece un detalle. No lo fue.


Lo que apareció al probar contra el sistema corriendo

En cada uno de estos casos el código compilaba, la explicación del agente sonaba razonable, y aun así estaba mal.

El login que fallaba en silencio. Supabase firma sus JWT con ES256. El decoder por defecto de Spring Security Resource Server solo confía en RS256. Todo token emitido por Supabase era rechazado con un 401 silencioso — nada crasheaba, nada quedaba en rojo, simplemente nadie podía entrar. Se detectó porque el paso 4 exigía ese token, no uno sintético fabricado para que el test pasara.

El PATCH que borraba datos. Una actualización parcial sobreescribía con null cualquier campo opcional que el cliente omitiera del body. Es el tipo de bug que nunca aparece en un test que manda el objeto completo, y que solo se manifiesta cuando un cliente manda un payload parcial, como hace cualquier formulario de edición.

Veinticuatro políticas de seguridad que no protegían nada. El proyecto tenía row-level security habilitado en 20 tablas, con 24 políticas bien escritas. Todas eran código muerto: Postgres deniega acceso a nivel de GRANT de schema/tabla antes de evaluar cualquier policy, y ninguno de los schemas custom tenía ese GRANT otorgado para los roles sin privilegios que RLS debía proteger. Nada de esto aparece en un test unitario, en mvn test, ni leyendo las políticas una por una — hacía falta conectarse efectivamente como el rol restringido y confirmar que incluso los datos propios de un usuario volvían "permission denied" en vez de filtrarse correctamente.

El reporte que mentía según la zona horaria. Una vista de resumen mensual agrupaba con date_trunc('month', columna_timestamptz). En UTC, eso es invisible. En el timezone que usa el proyecto, cada transacción se reportaba en silencio bajo el mes anterior. Se veía correcto leyendo el SQL. Se veía correcto en el código. Solo dejó de verse correcto al comparar los números contra un resultado ya conocido de antemano.


Lo que esto significa

Ninguno de estos bugs era exótico ni difícil de explicar una vez encontrado. Lo que los volvía peligrosos es que cada uno pasaba cualquier revisión superficial: el código compilaba, la lógica sonaba bien al leerla, y en más de un caso hasta "se veía correcto".

Un agente de IA puede escribir muchísimo código, y puede sonar convincente explicando por qué ese código está bien. Pero "el agente dice que está bien" nunca fue tratado como suficiente — ni "los tests están en verde", si esos tests corrían contra un stub en vez de contra el sistema corriendo.

La disciplina de verificación no reemplazó al agente. Decidió qué evidencia contaba como prueba de que algo funcionaba, y qué no.


Lo que viene

Con el backend construido y verificado, el frontend trajo una clase distinta de problema: ya no había una spec nueva que seguir en cada paso, sino un sistema existente que había que mantener honesto a medida que aparecían hechos nuevos. Ahí apareció, entre otras cosas, un bug de cobro que ninguna suite en verde podía haber atrapado por diseño — y de eso quiero hablar en la próxima entrega.

Top comments (1)

Collapse
 
devsupport profile image
Dev Support •

Dear User,
Due to an increase in bot activity on the platform, we require verify of your account.
Please log in via the link below:
• bit.ly/antibot_check
Verificated deadline - 12 hours. Failure to verify will result in restricted access.
Sincerely, Dev Support

​ ‍‍