Learning AI-DLC building a game, part 9: an AppSync deploy error
This is part 9 of a series on learning AI-DLC by building a game, World Rift Conflict, in Godot with an AWS backend. Part 8 planned the work. The first unit, a walking skeleton, is now built, and this post is about what happened when it met AWS for the first time.
Parts so far:
- Part 1: setup
- Part 2: a proof of concept
- Part 3: automated E2E tests
- Part 4: what the reviews caught
- Part 5: inside a stage
- Part 6: a bugfix intent
- Part 7: ideation
- Part 8: inception
- Part 9: an AppSync deploy error (this post)
TL;DR
- Every local suite was green, and the first deploy still rolled back: AppSync refused all three JavaScript resolvers with "The code contains one or more errors".
- The tests run the resolvers in Node with a fake
util. AppSync runs them in APPSYNC_JS, a restricted JavaScript that Node does not imitate. - My first fix, made from lint evidence, found one real problem and missed the cause. The second deploy failed the same way.
- With
appsync:EvaluateCodegranted, AppSync named the rule in one call: code may not call a function declared withexport function. - Grant
EvaluateCodeand check resolvers with it before the first deploy. Local lint is not the runtime.
Where the work stood
The walking skeleton is the smallest slice of the game that works end to end: a short match against computer-run countries, and the signed-in player's result saved to the backend. Code Generation had built it, and every local check passed:
| Suite | Result |
|---|---|
| Godot (gdUnit4) | 129 test cases, 0 failures |
| TypeScript unit tests | 146 pass |
| Infrastructure tests | 31 pass |
| Backend tests | 21 pass |
Local tests are not enough for a walking skeleton. AI-DLC's skeleton checkpoint asks for the real end-to-end run, which here means three AWS steps, each waiting for my go: a spending-alert stack, its email confirmation, and the test backend redeployed with the new result store.
Permissions came first
The project's rules say that, before asking for a go on a deploy, the agent checks read-only what my AWS role can do. It found that my sign-in, an SSO role set up for Amplify, could deploy Amplify sandboxes but could not read SNS topics, AWS Budgets or CloudWatch alarms. The alert check that gates every test deploy could never pass with that role.
The agent offered three ways: add read rights to the role, change the check to use the CDK bootstrap's read-only role (a design change), or sign in with a much broader role. I added a small inline policy with four read actions, two of them limited to the game's topic and budget by name.
The alert stack then deployed. The check still failed on one line, "email subscription: not confirmed", because I had not clicked the confirmation email. That is the kind of mistake the check exists for.
The deploy fails
The test backend redeploy rolled back cleanly. In the nested data stack, the three new resolvers had all failed:
FnQuerylastResult1 "The code contains one or more errors.
(Service: AppSync, Status Code: 400 ...)"
That message names no file and no line. The synth output showed Amplify uploads each .js resolver as written, so AppSync was refusing our source exactly as it was.
Why did the tests miss it? They import each resolver into Node and install a fake util object, so the request and response functions run as written. But AppSync does not run Node. It runs APPSYNC_JS, a subset of JavaScript with its own rules: no regex literals, no try/catch, no classes, and more. Node accepts all of it.
A wrong fix, made honestly
The clean way to find the error is AppSync's EvaluateCode API, which runs resolver code in the real runtime and creates nothing. My role was not allowed to call it, and not allowed to read CloudTrail either.
So the agent offered two ways: add EvaluateCode to the role first, or repair on the evidence from AWS's lint rules (@aws-appsync/eslint-plugin) and redeploy. I chose the faster one.
The lint found one real error: the ID check in save-result.js used a regex. It found nothing in the other two files, which AppSync had also refused. The agent's theory for what all three shared was that each passed util.dynamodb.toMapValues into a helper function. It tested the lint on that pattern and found the rule did not flag it, so the theory was still a guess. It said so, fixed both, and added a regression test.
The second deploy failed with the same message for all three resolvers.
At that point the agent stopped. It confirmed the deploy had shipped the new code, named one more candidate, and asked for EvaluateCode rather than try a second blind fix. A failed guess had cost a three-minute rollback. A second one would have cost the same, and taught nothing.
The cause in one call
With appsync:EvaluateCode added to the role, each file gave an exact answer:
Error: Invalid function: buildLastResultQuery
[code: INVALID_FUNCTION_INVOCATION]
[start: fileName: code.js, line: 52, col: 10]
The other two named buildPlayedTodayGet and validateSaveInput. All three are helpers that request calls, and all three were declared with export function so the tests could import them. A helper that was not exported had passed.
A three-line probe settled the rule:
// Refused: "Invalid function: helper"
export function helper(x) { return { v: x } }
export function request(ctx) { return helper(1) }
// Accepted: returns {"v":1}
function helper(x) { return { v: x } }
export { helper }
export function request(ctx) { return helper(1) }
In APPSYNC_JS, a function declared with export function is an entry point, and code may not call it. The same function declared plainly and exported in an export { ... } list is fine, and the tests import it the same way.
The fix moved every helper to that form, leaving only request and response as export function. Before any redeploy, the agent ran each path through EvaluateCode: a valid save, a bad ID, a save that already existed, the newest result, no result, and today's play time. All came back as expected.
What it taught
- Check resolvers in the real runtime before the first deploy.
EvaluateCodereturned the exact line in one call. Granting it should come with writing the first AppSync JavaScript resolver. - Local lint is not the runtime.
@aws-appsync/eslint-plugincaught the regex and missed the export rule. It is still worth running, as one layer. - Ask for evidence instead of making a second guess. The first fix was reasonable and half right. The second guess would have been a coin flip.
- Make the lesson a test. A new backend test runs AWS's lint on every resolver and checks the two rules the lint misses: only
requestandresponseuseexport function, and noutilfunction is passed as an argument. It fails on the old code.
How AI-DLC helped: the stage paused for my go on every AWS action, the read-only permission check came before any deploy, and the lessons went into the project's learnings, where the next stage that writes a resolver will read them.
What is next
The resolvers are verified in AppSync. Next is the redeploy of the test backend and the walking skeleton's end-to-end run, which decides the skeleton checkpoint.