Aprender AI-DLC creando un juego, parte 9: un error de AppSync
Esta es la parte 9 de una serie sobre aprender AI-DLC creando un juego, World Rift Conflict, en Godot con un backend en AWS. La parte 8 planificó el trabajo. La primera unidad, un esqueleto funcional, ya está construida, y este artículo cuenta qué pasó cuando se encontró con AWS por primera vez.
Partes hasta ahora:
- Parte 1: configuración
- Parte 2: prueba de concepto
- Parte 3: pruebas E2E automatizadas
- Parte 4: lo que hallaron las revisiones
- Parte 5: dentro de una etapa
- Parte 6: un bugfix
- Parte 7: ideación
- Parte 8: incepción
- Parte 9: un error de AppSync (este artículo)
Resumen
- Todas las pruebas locales estaban en verde, y aun así el primer despliegue se revirtió: AppSync rechazó los tres resolvers JavaScript con "The code contains one or more errors".
- Las pruebas ejecutan los resolvers en Node con un
utilfalso. AppSync los ejecuta en APPSYNC_JS, un JavaScript restringido que Node no imita. - Mi primer arreglo, hecho con la evidencia del lint, encontró un problema real y no dio con la causa. El segundo despliegue falló igual.
- Con permiso para
appsync:EvaluateCode, AppSync nombró la regla en una sola llamada: el código no puede llamar a una función declarada conexport function. - Da permiso para
EvaluateCodey revisa los resolvers con él antes del primer despliegue. El lint local no es el runtime.
Dónde estaba el trabajo
El esqueleto funcional es la porción más pequeña del juego que funciona de punta a punta: una partida corta contra países controlados por la computadora, y el resultado del jugador conectado guardado en el backend. Code Generation lo había construido, y todas las pruebas locales pasaban:
| Conjunto | Resultado |
|---|---|
| Godot (gdUnit4) | 129 casos de prueba, 0 fallos |
| Pruebas unitarias TypeScript | 146 pasan |
| Pruebas de infraestructura | 31 pasan |
| Pruebas del backend | 21 pasan |
Para un esqueleto funcional, las pruebas locales no bastan. El punto de control del esqueleto en AI-DLC pide la ejecución real de punta a punta, que aquí significa tres pasos en AWS, cada uno esperando mi aprobación: una pila de alertas de gasto, la confirmación de su correo, y el backend de pruebas desplegado de nuevo con el nuevo almacén de resultados.
Primero, los permisos
Las reglas del proyecto dicen que, antes de pedir aprobación para un despliegue, el agente revisa en modo de solo lectura qué puede hacer mi rol de AWS. Encontró que mi acceso, un rol SSO preparado para Amplify, podía desplegar sandboxes de Amplify pero no podía leer temas de SNS, AWS Budgets ni alarmas de CloudWatch. La revisión de alertas que protege cada despliegue de pruebas nunca podría pasar con ese rol.
El agente ofreció tres caminos: añadir permisos de lectura al rol, cambiar la revisión para usar el rol de solo lectura del bootstrap de CDK (un cambio de diseño), o entrar con un rol mucho más amplio. Añadí una pequeña política en línea con cuatro acciones de lectura, dos de ellas limitadas por nombre al tema y al presupuesto del juego.
Después se desplegó la pila de alertas. La revisión seguía fallando en una línea, "email subscription: not confirmed", porque yo no había hecho clic en el correo de confirmación. Para ese tipo de descuido existe la revisión.
El despliegue falla
El nuevo despliegue del backend de pruebas se revirtió sin dejar restos. En la pila anidada de datos, los tres resolvers nuevos habían fallado:
FnQuerylastResult1 "The code contains one or more errors.
(Service: AppSync, Status Code: 400 ...)"
Ese mensaje no nombra ningún archivo ni ninguna línea. La salida de synth mostraba que Amplify sube cada resolver .js tal como está escrito, así que AppSync rechazaba nuestro código fuente exactamente como era.
¿Por qué no lo detectaron las pruebas? Importan cada resolver en Node e instalan un objeto util falso, así que las funciones request y response se ejecutan tal como están escritas. Pero AppSync no ejecuta Node. Ejecuta APPSYNC_JS, un subconjunto de JavaScript con sus propias reglas: sin expresiones regulares literales, sin try/catch, sin clases, y más. Node lo acepta todo.
Un arreglo equivocado, hecho con honestidad
La forma limpia de encontrar el error es la API EvaluateCode de AppSync, que ejecuta el código de un resolver en el runtime real y no crea nada. Mi rol no tenía permiso para llamarla, ni para leer CloudTrail.
Así que el agente ofreció dos caminos: añadir primero EvaluateCode al rol, o reparar con la evidencia de las reglas de lint de AWS (@aws-appsync/eslint-plugin) y desplegar de nuevo. Elegí el más rápido.
El lint encontró un error real: la validación de IDs en save-result.js usaba una expresión regular. No encontró nada en los otros dos archivos, que AppSync también había rechazado. La teoría del agente sobre lo que compartían los tres era que cada uno pasaba util.dynamodb.toMapValues a una función auxiliar. Probó el lint con ese patrón y vio que la regla no lo marcaba, así que la teoría seguía siendo una suposición. Lo dijo, arregló ambas cosas y añadió una prueba de regresión.
El segundo despliegue falló con el mismo mensaje en los tres resolvers.
Ahí el agente se detuvo. Confirmó que el despliegue había subido el código nuevo, nombró un candidato más, y pidió EvaluateCode en vez de intentar un segundo arreglo a ciegas. Una suposición fallida había costado una reversión de tres minutos. Una segunda habría costado lo mismo, sin enseñar nada.
La causa en una llamada
Con appsync:EvaluateCode añadido al rol, cada archivo dio una respuesta exacta:
Error: Invalid function: buildLastResultQuery
[code: INVALID_FUNCTION_INVOCATION]
[start: fileName: code.js, line: 52, col: 10]
Los otros dos nombraron buildPlayedTodayGet y validateSaveInput. Los tres son funciones auxiliares que llama request, y los tres estaban declarados con export function para que las pruebas pudieran importarlos. Una función auxiliar que no se exportaba había pasado.
Una prueba de tres líneas confirmó la regla:
// Rechazado: "Invalid function: helper"
export function helper(x) { return { v: x } }
export function request(ctx) { return helper(1) }
// Aceptado: devuelve {"v":1}
function helper(x) { return { v: x } }
export { helper }
export function request(ctx) { return helper(1) }
En APPSYNC_JS, una función declarada con export function es un punto de entrada, y el código no puede llamarla. La misma función declarada de forma normal y exportada en una lista export { ... } funciona, y las pruebas la importan igual.
El arreglo pasó todas las funciones auxiliares a esa forma, y dejó solo request y response como export function. Antes de desplegar de nuevo, el agente ejecutó cada camino con EvaluateCode: un guardado válido, un ID incorrecto, un guardado que ya existía, el resultado más reciente, ningún resultado, y el tiempo de juego del día. Todos respondieron como se esperaba.
Lo que enseñó
- Revisa los resolvers en el runtime real antes del primer despliegue.
EvaluateCodedevolvió la línea exacta en una llamada. El permiso debería llegar junto con el primer resolver JavaScript de AppSync. - El lint local no es el runtime.
@aws-appsync/eslint-plugindetectó la expresión regular y no vio la regla de los exports. Sigue valiendo la pena como una capa más. - Pide evidencia en vez de una segunda suposición. El primer arreglo era razonable y acertó a medias. La segunda suposición habría sido lanzar una moneda.
- Convierte la lección en una prueba. Una nueva prueba del backend ejecuta el lint de AWS en cada resolver y revisa las dos reglas que el lint no ve: solo
requestyresponseusanexport function, y ninguna función deutilse pasa como argumento. Falla con el código antiguo.
Cómo ayudó AI-DLC: la etapa se detuvo para pedir mi aprobación en cada acción de AWS, la revisión de permisos en solo lectura llegó antes de cualquier despliegue, y las lecciones quedaron en los aprendizajes del proyecto, donde las leerá la próxima etapa que escriba un resolver.
Lo que sigue
Los resolvers están verificados en AppSync. Lo siguiente es desplegar de nuevo el backend de pruebas y la ejecución de punta a punta del esqueleto funcional, que decide su punto de control.