diff --git a/examples/aws-lambda/template.yaml b/examples/aws-lambda/template.yaml index 41d0bf0208a..fc28dc1f0b2 100644 --- a/examples/aws-lambda/template.yaml +++ b/examples/aws-lambda/template.yaml @@ -266,6 +266,11 @@ Resources: SelectPlanProtocol: Type: Choice Choices: + # A comparison on a missing path is a States.Runtime error, not a non-match, so an + # omitted PlanProtocol has to be caught before any StringEquals rule. + - Variable: $.PlanProtocol + IsPresent: false + Next: !If [HasVideoFunction, WarmRender, PlanV2] # Only a stack with the arm64 video function has a cold x86 function to warm. # Without one the compared value can never match. - Variable: $.PlanProtocol diff --git a/packages/aws-lambda/src/cdk/HyperframesRenderStack.snapshot.test.ts b/packages/aws-lambda/src/cdk/HyperframesRenderStack.snapshot.test.ts index 29005916e9c..d8d69359dcc 100644 --- a/packages/aws-lambda/src/cdk/HyperframesRenderStack.snapshot.test.ts +++ b/packages/aws-lambda/src/cdk/HyperframesRenderStack.snapshot.test.ts @@ -185,6 +185,26 @@ describe("HyperframesRenderStack — snapshot", () => { } }); + // Step Functions raises States.Runtime for a comparison on a missing path instead of moving on, + // so the Default branch is unreachable unless presence is checked before any comparison. + it("routes an omitted plan protocol before any rule compares it", () => { + for (const [name, definition] of [ + ["CDK", SYNTHED.definition], + ["SAM", readSamDefinition()], + ] as const) { + const choices = requireRecord(definition.States.SelectPlanProtocol, "SelectPlanProtocol") + .Choices as Record[]; + expect({ name, first: { ...choices[0], Next: undefined } }).toEqual({ + name, + first: { Variable: "$.PlanProtocol", IsPresent: false, Next: undefined }, + }); + } + expect(SYNTHED.definition.States.SelectPlanProtocol.Choices[0].Next).toBe("PlanV2"); + const samFirst = (readSamDefinition().States.SelectPlanProtocol as Record) + .Choices as Record[]; + expect(samFirst[0].Next).toEqual(["HasVideoFunction", "WarmRender", "PlanV2"]); + }); + it("preserves every typed non-retryable error name across the three Lambda tasks", () => { const { definition } = SYNTHED; const collected = new Set(); diff --git a/packages/aws-lambda/src/cdk/HyperframesRenderStack.ts b/packages/aws-lambda/src/cdk/HyperframesRenderStack.ts index 1eddfbe79d4..48ca06a2f35 100644 --- a/packages/aws-lambda/src/cdk/HyperframesRenderStack.ts +++ b/packages/aws-lambda/src/cdk/HyperframesRenderStack.ts @@ -443,7 +443,10 @@ export class HyperframesRenderStack extends Construct { error: "PLAN_PROTOCOL_UNSUPPORTED", cause: 'PlanProtocol must be "v2" or absent (defaults to v2).', }); + // A comparison on a missing path is a States.Runtime error, not a non-match, so an omitted + // PlanProtocol has to be caught before the stringEquals rule. return new sfn.Choice(this, "SelectPlanProtocol") + .when(sfn.Condition.isNotPresent("$.PlanProtocol"), planV2) .when(sfn.Condition.stringEquals("$.PlanProtocol", "v2"), planV2) .when(sfn.Condition.isPresent("$.PlanProtocol"), unsupportedPlanProtocol) .otherwise(planV2);