@kensio/yulin 1.20.15 → 1.20.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/sdk/module/sim-sdk-module-client-interceptor.d.ts +5 -0
- package/dist/sdk/module/sim-sdk-module-client-interceptor.js +14 -0
- package/dist/sdk/module/sim-sdk-static-client-factory.d.ts +26 -0
- package/dist/sdk/module/sim-sdk-static-client-factory.js +59 -0
- package/dist/sdk/send-patch.d.ts +8 -0
- package/dist/sdk/send-patch.js +11 -0
- package/dist/service/aws/caller/sim-aws-caller-resolver.d.ts +28 -6
- package/dist/service/aws/caller/sim-aws-caller-resolver.js +18 -11
- package/dist/service/aws/caller/sim-aws-caller.d.ts +9 -0
- package/dist/service/aws/caller/sim-aws-resolved-caller.d.ts +19 -0
- package/dist/service/aws/caller/sim-aws-resolved-caller.js +26 -0
- package/dist/service/aws/factory/sim-aws-account-region-service-builder.d.ts +6 -0
- package/dist/service/aws/factory/sim-aws-account-region-service-builder.js +3 -0
- package/dist/service/aws/factory/sim-aws-account-service-cache.d.ts +6 -0
- package/dist/service/aws/factory/sim-aws-account-service-cache.js +5 -0
- package/dist/service/aws/factory/sim-aws-service-factory.d.ts +5 -0
- package/dist/service/aws/factory/sim-aws-service-factory.js +6 -0
- package/dist/service/aws/sim-aws-properties.d.ts +15 -0
- package/dist/service/aws/sim-aws.js +2 -1
- package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.d.ts +30 -7
- package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.js +20 -11
- package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.d.ts +2 -1
- package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.js +5 -1
- package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.d.ts +10 -0
- package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.js +15 -3
- package/dist/service/iam/authorize/sim-iam-account-auth-z.d.ts +11 -4
- package/dist/service/iam/authorize/sim-iam-account-auth-z.js +5 -3
- package/dist/service/iam/authorize/sim-iam-authorizer.d.ts +2 -1
- package/dist/service/iam/authorize/sim-iam-authorizer.js +1 -0
- package/dist/service/iam/role/sim-iam-role-with-policy.factory.d.ts +9 -0
- package/dist/service/iam/role/sim-iam-role-with-policy.factory.js +4 -2
- package/dist/service/iam/sim-iam-account-parts.d.ts +8 -0
- package/dist/service/iam/sim-iam-account-parts.js +1 -0
- package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.d.ts +9 -2
- package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.js +2 -2
- package/dist/service/sts/auth-z/assume-role-source-account-auth-z.d.ts +10 -4
- package/dist/service/sts/auth-z/assume-role-source-account-auth-z.js +8 -7
- package/dist/service/sts/auth-z/assume-role-target-auth-z.d.ts +2 -2
- package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.d.ts +6 -2
- package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.js +14 -11
- package/dist/service/sts/command/assume-role/assume-role.handler.d.ts +5 -1
- package/dist/service/sts/command/assume-role/assume-role.handler.js +5 -2
- package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.d.ts +5 -1
- package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.js +4 -1
- package/dist/service/sts/service-role/sim-service-role.js +5 -1
- package/dist/service/sts/sim-sts.d.ts +9 -1
- package/dist/service/sts/sim-sts.js +4 -0
- package/docs/README.md +54 -0
- package/docs/ai-skill/README.md +69 -0
- package/docs/cli/README.md +284 -0
- package/docs/factories/README.md +140 -0
- package/docs/lint/README.md +176 -0
- package/docs/non-aws-dependencies/README.md +261 -0
- package/docs/sdk/README.md +272 -0
- package/docs/serve/README.md +1126 -0
- package/docs/services/acm/README.md +823 -0
- package/docs/services/apigateway/README.md +1919 -0
- package/docs/services/apigatewayv2/README.md +2678 -0
- package/docs/services/athena/README.md +1044 -0
- package/docs/services/bedrock/README.md +350 -0
- package/docs/services/cloudformation/README.md +3440 -0
- package/docs/services/cloudfront/README.md +2874 -0
- package/docs/services/cloudwatch/README.md +420 -0
- package/docs/services/cognito/README.md +4923 -0
- package/docs/services/dynamodb/README.md +3784 -0
- package/docs/services/ecr/README.md +302 -0
- package/docs/services/ecs/README.md +2010 -0
- package/docs/services/elbv2/README.md +1897 -0
- package/docs/services/eventbridge/README.md +1110 -0
- package/docs/services/firehose/README.md +805 -0
- package/docs/services/glue/README.md +501 -0
- package/docs/services/iam/README.md +1283 -0
- package/docs/services/kinesis/README.md +454 -0
- package/docs/services/kms/README.md +659 -0
- package/docs/services/lambda/README.md +3645 -0
- package/docs/services/logs/README.md +868 -0
- package/docs/services/organizations/README.md +574 -0
- package/docs/services/personalize/README.md +835 -0
- package/docs/services/rekognition/README.md +1070 -0
- package/docs/services/route53/README.md +1783 -0
- package/docs/services/s3/README.md +3024 -0
- package/docs/services/scheduler/README.md +806 -0
- package/docs/services/secretsmanager/README.md +667 -0
- package/docs/services/ses/README.md +1011 -0
- package/docs/services/sns/README.md +1904 -0
- package/docs/services/sqs/README.md +1185 -0
- package/docs/services/ssm/README.md +1072 -0
- package/docs/services/stepfunctions/README.md +1332 -0
- package/docs/services/sts/README.md +245 -0
- package/docs/services/wafv2/README.md +1378 -0
- package/docs/terraform/README.md +248 -0
- package/docs/time/README.md +284 -0
- package/llms.txt +59 -0
- package/package.json +7 -4
|
@@ -0,0 +1,1332 @@
|
|
|
1
|
+
# Simulated Step Functions
|
|
2
|
+
|
|
3
|
+
Simulated Step Functions interprets Amazon States Language and runs a state machine in the same
|
|
4
|
+
process as the code under test. A workflow held in a template as data becomes something a test can
|
|
5
|
+
run and assert on.
|
|
6
|
+
|
|
7
|
+
Types for simulated Step Functions are imported from the `@kensio/yulin/stepfunctions` subpath.
|
|
8
|
+
|
|
9
|
+
## What runs today
|
|
10
|
+
|
|
11
|
+
Every state type Amazon States Language defines runs. `Pass`, `Task`, `Succeed`, `Fail`, `Choice`,
|
|
12
|
+
`Wait`, `Parallel` and `Map`.
|
|
13
|
+
|
|
14
|
+
The data-flow fields run in full. `InputPath`, `Parameters`, `ResultSelector`, `ResultPath` and
|
|
15
|
+
`OutputPath` apply in that order, reading Reference Paths and the intrinsic functions.
|
|
16
|
+
|
|
17
|
+
A `Task` state invokes a simulated Lambda function, calls an operation on any other simulated
|
|
18
|
+
service, or starts another state machine. A `Resource` this simulator has no answer for is refused
|
|
19
|
+
when the state machine is created, naming what the definition asked for.
|
|
20
|
+
|
|
21
|
+
A `Task` state's `Retry` and `Catch` both run, on the simulation's clock, along with the
|
|
22
|
+
`TimeoutSeconds` and `HeartbeatSeconds` it takes. A `Parallel` state and a `Map` state take the same
|
|
23
|
+
`Retry` and `Catch`.
|
|
24
|
+
|
|
25
|
+
The context object runs, so `$$.Execution`, `$$.StateMachine`, `$$.State` and `$$.Map` are all
|
|
26
|
+
readable.
|
|
27
|
+
|
|
28
|
+
## Running a state machine
|
|
29
|
+
|
|
30
|
+
```typescript sim-step-functions-run
|
|
31
|
+
/**
|
|
32
|
+
* Creating a state machine and running an execution against it.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { SimAws } from "@kensio/yulin";
|
|
36
|
+
|
|
37
|
+
const simAws = new SimAws();
|
|
38
|
+
|
|
39
|
+
const created = await simAws.stepFunctions().createStateMachine({
|
|
40
|
+
input: {
|
|
41
|
+
name: "Enrolment",
|
|
42
|
+
roleArn: "arn:aws:iam::123456789012:role/WorkflowRole",
|
|
43
|
+
definition: JSON.stringify({
|
|
44
|
+
StartAt: "Record",
|
|
45
|
+
States: {
|
|
46
|
+
Record: {
|
|
47
|
+
Type: "Pass",
|
|
48
|
+
Result: { enrolled: true },
|
|
49
|
+
ResultPath: "$.outcome",
|
|
50
|
+
Next: "Done",
|
|
51
|
+
},
|
|
52
|
+
Done: { Type: "Succeed" },
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const started = await simAws.stepFunctions().startExecution({
|
|
59
|
+
input: {
|
|
60
|
+
stateMachineArn: created.stateMachineArn,
|
|
61
|
+
input: JSON.stringify({ student: "Wei" }),
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const described = await simAws
|
|
66
|
+
.stepFunctions()
|
|
67
|
+
.describeExecution({ input: { executionArn: started.executionArn } });
|
|
68
|
+
|
|
69
|
+
console.log(described.status); // SUCCEEDED
|
|
70
|
+
console.log(described.output); // {"student":"Wei","outcome":{"enrolled":true}}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Asserting on the states an execution visited
|
|
74
|
+
|
|
75
|
+
`DescribeExecution` says how an execution ended. The route it took is read from the simulator's own
|
|
76
|
+
inspection accessor:
|
|
77
|
+
|
|
78
|
+
```typescript sim-step-functions-visited-states
|
|
79
|
+
/**
|
|
80
|
+
* Reading back which states an execution went through.
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
import { SimAws } from "@kensio/yulin";
|
|
84
|
+
|
|
85
|
+
const simAws = new SimAws();
|
|
86
|
+
|
|
87
|
+
const created = await simAws.stepFunctions().createStateMachine({
|
|
88
|
+
input: {
|
|
89
|
+
name: "Enrolment",
|
|
90
|
+
roleArn: "arn:aws:iam::123456789012:role/WorkflowRole",
|
|
91
|
+
definition: JSON.stringify({
|
|
92
|
+
StartAt: "Check",
|
|
93
|
+
States: {
|
|
94
|
+
Check: { Type: "Pass", Next: "Decline" },
|
|
95
|
+
Decline: { Type: "Fail", Error: "NotEligible", Cause: "No place left" },
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const started = await simAws
|
|
102
|
+
.stepFunctions()
|
|
103
|
+
.startExecution({ input: { stateMachineArn: created.stateMachineArn } });
|
|
104
|
+
|
|
105
|
+
console.log(
|
|
106
|
+
simAws.stepFunctions().inspection().visitedStates(started.executionArn),
|
|
107
|
+
); // [ 'Check', 'Decline' ]
|
|
108
|
+
|
|
109
|
+
const described = await simAws
|
|
110
|
+
.stepFunctions()
|
|
111
|
+
.describeExecution({ input: { executionArn: started.executionArn } });
|
|
112
|
+
|
|
113
|
+
console.log(described.status); // FAILED
|
|
114
|
+
console.log(described.error); // NotEligible
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
A failing execution is recorded on the execution, and the call returns as it would for one that
|
|
118
|
+
succeeded. Simulated EventBridge treats an undeliverable event the same way. An execution failing is
|
|
119
|
+
as often the thing under test as it is a fault, and raising it would fail an unrelated `advanceBy`
|
|
120
|
+
elsewhere in the same test.
|
|
121
|
+
|
|
122
|
+
## Invoking a Lambda function
|
|
123
|
+
|
|
124
|
+
A `Task` state invokes a simulated Lambda function, through either of the two `Resource` forms CDK's
|
|
125
|
+
`LambdaInvoke` emits.
|
|
126
|
+
|
|
127
|
+
`arn:aws:states:::lambda:invoke` is the integration Step Functions optimises. The state is talking to
|
|
128
|
+
the Lambda API, so its `Parameters` are an `Invoke` request (`FunctionName` names the function and
|
|
129
|
+
`Payload` carries what it is sent) and its result is an `Invoke` response, with the handler's answer
|
|
130
|
+
under `Payload`.
|
|
131
|
+
|
|
132
|
+
A function ARN sends the state's own input to the handler and answers with what the handler
|
|
133
|
+
returned. CDK writes this form for `payloadResponseOnly`.
|
|
134
|
+
|
|
135
|
+
```typescript sim-step-functions-task
|
|
136
|
+
/**
|
|
137
|
+
* A workflow whose Task states invoke simulated Lambda functions.
|
|
138
|
+
*/
|
|
139
|
+
|
|
140
|
+
import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
|
|
141
|
+
import { CreateFunctionCommand } from "@aws-sdk/client-lambda";
|
|
142
|
+
|
|
143
|
+
import { SimAws } from "@kensio/yulin";
|
|
144
|
+
import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
|
|
145
|
+
|
|
146
|
+
const simAws = new SimAws();
|
|
147
|
+
|
|
148
|
+
await simAws.lambda().createFunction(
|
|
149
|
+
new CreateFunctionCommand({
|
|
150
|
+
FunctionName: "check-enrolment",
|
|
151
|
+
Role: "arn:aws:iam::123456789012:role/FunctionRole",
|
|
152
|
+
Code: {
|
|
153
|
+
ZipFile: makeLambdaZipFileInput((event: { term: number }) => ({
|
|
154
|
+
eligible: event.term > 1,
|
|
155
|
+
})),
|
|
156
|
+
},
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const enrol = await simAws.lambda().createFunction(
|
|
161
|
+
new CreateFunctionCommand({
|
|
162
|
+
FunctionName: "enrol-student",
|
|
163
|
+
Role: "arn:aws:iam::123456789012:role/FunctionRole",
|
|
164
|
+
Code: {
|
|
165
|
+
ZipFile: makeLambdaZipFileInput((event: { student: string }) => ({
|
|
166
|
+
enrolled: event.student,
|
|
167
|
+
})),
|
|
168
|
+
},
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// The execution assumes this role, and invokes both functions as it.
|
|
173
|
+
const role = await simAws.iam().createRole(
|
|
174
|
+
new CreateRoleCommand({
|
|
175
|
+
RoleName: "WorkflowRole",
|
|
176
|
+
AssumeRolePolicyDocument: JSON.stringify({
|
|
177
|
+
Version: "2012-10-17",
|
|
178
|
+
Statement: {
|
|
179
|
+
Effect: "Allow",
|
|
180
|
+
Principal: { Service: "states.amazonaws.com" },
|
|
181
|
+
Action: "sts:AssumeRole",
|
|
182
|
+
},
|
|
183
|
+
}),
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
await simAws.iam().putRolePolicy(
|
|
188
|
+
new PutRolePolicyCommand({
|
|
189
|
+
RoleName: "WorkflowRole",
|
|
190
|
+
PolicyName: "InvokeEnrolmentFunctions",
|
|
191
|
+
PolicyDocument: JSON.stringify({
|
|
192
|
+
Version: "2012-10-17",
|
|
193
|
+
Statement: {
|
|
194
|
+
Effect: "Allow",
|
|
195
|
+
Action: "lambda:InvokeFunction",
|
|
196
|
+
Resource: "*",
|
|
197
|
+
},
|
|
198
|
+
}),
|
|
199
|
+
}),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const created = await simAws.stepFunctions().createStateMachine({
|
|
203
|
+
input: {
|
|
204
|
+
name: "Enrolment",
|
|
205
|
+
roleArn: role.Role.Arn,
|
|
206
|
+
definition: JSON.stringify({
|
|
207
|
+
StartAt: "Check",
|
|
208
|
+
States: {
|
|
209
|
+
Check: {
|
|
210
|
+
Type: "Task",
|
|
211
|
+
Resource: "arn:aws:states:::lambda:invoke",
|
|
212
|
+
Parameters: { FunctionName: "check-enrolment", "Payload.$": "$" },
|
|
213
|
+
ResultSelector: { "eligible.$": "$.Payload.eligible" },
|
|
214
|
+
ResultPath: "$.outcome",
|
|
215
|
+
Next: "Eligible",
|
|
216
|
+
},
|
|
217
|
+
Eligible: {
|
|
218
|
+
Type: "Choice",
|
|
219
|
+
Choices: [
|
|
220
|
+
{
|
|
221
|
+
Variable: "$.outcome.eligible",
|
|
222
|
+
BooleanEquals: true,
|
|
223
|
+
Next: "Enrol",
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
Default: "Decline",
|
|
227
|
+
},
|
|
228
|
+
Enrol: { Type: "Task", Resource: enrol.FunctionArn, End: true },
|
|
229
|
+
Decline: { Type: "Fail", Error: "NotEligible" },
|
|
230
|
+
},
|
|
231
|
+
}),
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const started = await simAws.stepFunctions().startExecution({
|
|
236
|
+
input: {
|
|
237
|
+
stateMachineArn: created.stateMachineArn,
|
|
238
|
+
input: JSON.stringify({ student: "Wei", term: 3 }),
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const described = await simAws
|
|
243
|
+
.stepFunctions()
|
|
244
|
+
.describeExecution({ input: { executionArn: started.executionArn } });
|
|
245
|
+
|
|
246
|
+
console.log(described.output); // {"enrolled":"Wei"}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The five data-flow fields apply around a task the way they apply around a `Pass` state.
|
|
250
|
+
`InputPath` and `Parameters` build what the task is sent, and `ResultSelector`, `ResultPath` and
|
|
251
|
+
`OutputPath` shape what comes back. `ResultPath` reads the state's raw input. `Check` above keeps
|
|
252
|
+
the student it was given, alongside the one field its `ResultSelector` picked out of the response.
|
|
253
|
+
|
|
254
|
+
A handler runs on the simulation's clock. A timestamp a handler stamps and a `TimestampPath` a later
|
|
255
|
+
`Wait` state reads agree.
|
|
256
|
+
|
|
257
|
+
`FunctionName` takes a function name or a function ARN, as the Lambda API does. A name alone is a
|
|
258
|
+
function in the state machine's own Account and Region, and an ARN can name one in another. A
|
|
259
|
+
qualified ARN invokes the version an alias points at.
|
|
260
|
+
|
|
261
|
+
### The execution role
|
|
262
|
+
|
|
263
|
+
The execution assumes the state machine's `RoleArn` and invokes the function as that role. The role
|
|
264
|
+
needs two things. Its trust policy admits `states.amazonaws.com`, and one of its policies allows
|
|
265
|
+
`lambda:InvokeFunction` on the function. A role missing either fails the task with
|
|
266
|
+
`States.TaskFailed`, saying which of the two it was.
|
|
267
|
+
|
|
268
|
+
The function's own resource policy is not consulted. A task arrives as an assumed role, and a role
|
|
269
|
+
in the same Account needs only its own identity policy. An EventBridge rule works the other way
|
|
270
|
+
round, arriving as a service principal that the function's resource policy admits.
|
|
271
|
+
|
|
272
|
+
### When a task fails
|
|
273
|
+
|
|
274
|
+
A handler that raises fails the task, and the Amazon States Language error name follows the
|
|
275
|
+
`Resource` form that invoked it:
|
|
276
|
+
|
|
277
|
+
- Through `arn:aws:states:::lambda:invoke` the failure is `States.TaskFailed`.
|
|
278
|
+
- Through a function ARN it is the handler's own error type. An error named `NotEligible` fails the
|
|
279
|
+
task as `NotEligible`.
|
|
280
|
+
|
|
281
|
+
`Retry` and `Catch` match on that name, and the next section covers both. A task that carries
|
|
282
|
+
neither ends the execution, and `DescribeExecution` carries the error name and the handler's
|
|
283
|
+
message.
|
|
284
|
+
|
|
285
|
+
Anything else that stops a task from running fails it with `States.TaskFailed`. A function that is
|
|
286
|
+
not there, a role that cannot be assumed, and a role that may not invoke are the three of them, and
|
|
287
|
+
the cause says which one it was.
|
|
288
|
+
|
|
289
|
+
## Retrying and catching a failure
|
|
290
|
+
|
|
291
|
+
A `Task` state's `Retry` runs a failing task again on the simulation's clock. Its `Catch` sends a
|
|
292
|
+
failure that survived the retries to another state. Both match on the Amazon States Language error
|
|
293
|
+
name, and both are read when the state machine is created.
|
|
294
|
+
|
|
295
|
+
Each attempt is scheduled at its own instant, and the execution reads as `RUNNING` in between. One
|
|
296
|
+
`advanceBy` covering the whole backoff runs every attempt in it, because an attempt the advance
|
|
297
|
+
schedules is itself due by the time the advance gets there. A test advances once and then asserts.
|
|
298
|
+
|
|
299
|
+
```typescript sim-step-functions-retry
|
|
300
|
+
/**
|
|
301
|
+
* Retrying a failing task on the clock, and catching what the retries leave.
|
|
302
|
+
*/
|
|
303
|
+
|
|
304
|
+
import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
|
|
305
|
+
import { CreateFunctionCommand } from "@aws-sdk/client-lambda";
|
|
306
|
+
|
|
307
|
+
import { SimAws, SimFixedClock } from "@kensio/yulin";
|
|
308
|
+
import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
|
|
309
|
+
|
|
310
|
+
const simAws = new SimAws({
|
|
311
|
+
clock: new SimFixedClock(new Date("2026-07-26T09:00:00.000Z")),
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// The enrolment service is down for the whole of this run.
|
|
315
|
+
await simAws.lambda().createFunction(
|
|
316
|
+
new CreateFunctionCommand({
|
|
317
|
+
FunctionName: "check-enrolment",
|
|
318
|
+
Role: "arn:aws:iam::123456789012:role/FunctionRole",
|
|
319
|
+
Code: {
|
|
320
|
+
ZipFile: makeLambdaZipFileInput(() => {
|
|
321
|
+
throw new Error("the enrolment service is down");
|
|
322
|
+
}),
|
|
323
|
+
},
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
const role = await simAws.iam().createRole(
|
|
328
|
+
new CreateRoleCommand({
|
|
329
|
+
RoleName: "WorkflowRole",
|
|
330
|
+
AssumeRolePolicyDocument: JSON.stringify({
|
|
331
|
+
Version: "2012-10-17",
|
|
332
|
+
Statement: {
|
|
333
|
+
Effect: "Allow",
|
|
334
|
+
Principal: { Service: "states.amazonaws.com" },
|
|
335
|
+
Action: "sts:AssumeRole",
|
|
336
|
+
},
|
|
337
|
+
}),
|
|
338
|
+
}),
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
await simAws.iam().putRolePolicy(
|
|
342
|
+
new PutRolePolicyCommand({
|
|
343
|
+
RoleName: "WorkflowRole",
|
|
344
|
+
PolicyName: "InvokeEnrolmentFunctions",
|
|
345
|
+
PolicyDocument: JSON.stringify({
|
|
346
|
+
Version: "2012-10-17",
|
|
347
|
+
Statement: {
|
|
348
|
+
Effect: "Allow",
|
|
349
|
+
Action: "lambda:InvokeFunction",
|
|
350
|
+
Resource: "*",
|
|
351
|
+
},
|
|
352
|
+
}),
|
|
353
|
+
}),
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
const created = await simAws.stepFunctions().createStateMachine({
|
|
357
|
+
input: {
|
|
358
|
+
name: "Enrolment",
|
|
359
|
+
roleArn: role.Role.Arn,
|
|
360
|
+
definition: JSON.stringify({
|
|
361
|
+
StartAt: "Check",
|
|
362
|
+
States: {
|
|
363
|
+
Check: {
|
|
364
|
+
Type: "Task",
|
|
365
|
+
Resource: "arn:aws:states:::lambda:invoke",
|
|
366
|
+
Parameters: { FunctionName: "check-enrolment", "Payload.$": "$" },
|
|
367
|
+
Retry: [
|
|
368
|
+
{
|
|
369
|
+
ErrorEquals: ["States.TaskFailed"],
|
|
370
|
+
IntervalSeconds: 2,
|
|
371
|
+
MaxAttempts: 2,
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
Catch: [
|
|
375
|
+
{
|
|
376
|
+
ErrorEquals: ["States.ALL"],
|
|
377
|
+
Next: "Compensate",
|
|
378
|
+
ResultPath: "$.error",
|
|
379
|
+
},
|
|
380
|
+
],
|
|
381
|
+
Next: "Enrol",
|
|
382
|
+
},
|
|
383
|
+
Enrol: { Type: "Pass", Result: { enrolled: true }, End: true },
|
|
384
|
+
Compensate: { Type: "Pass", End: true },
|
|
385
|
+
},
|
|
386
|
+
}),
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const started = await simAws.stepFunctions().startExecution({
|
|
391
|
+
input: {
|
|
392
|
+
stateMachineArn: created.stateMachineArn,
|
|
393
|
+
input: JSON.stringify({ student: "Wei" }),
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const waiting = await simAws
|
|
398
|
+
.stepFunctions()
|
|
399
|
+
.describeExecution({ input: { executionArn: started.executionArn } });
|
|
400
|
+
|
|
401
|
+
console.log(waiting.status); // RUNNING
|
|
402
|
+
|
|
403
|
+
// The attempts fall at 0, 2 and 6 seconds. One advance covers all three.
|
|
404
|
+
await simAws.clock().advanceBy({ seconds: 10 });
|
|
405
|
+
|
|
406
|
+
const described = await simAws
|
|
407
|
+
.stepFunctions()
|
|
408
|
+
.describeExecution({ input: { executionArn: started.executionArn } });
|
|
409
|
+
|
|
410
|
+
console.log(described.status); // SUCCEEDED
|
|
411
|
+
console.log(described.stopDate); // 2026-07-26T09:00:06.000Z
|
|
412
|
+
|
|
413
|
+
// {"student":"Wei","error":{"Error":"States.TaskFailed","Cause":"The function
|
|
414
|
+
// the Task state Check invoked raised Error: the enrolment service is down"}}
|
|
415
|
+
console.log(described.output);
|
|
416
|
+
|
|
417
|
+
// [ { stateName: 'Check', error: 'States.TaskFailed' },
|
|
418
|
+
// { stateName: 'Check', error: 'States.TaskFailed' },
|
|
419
|
+
// { stateName: 'Check', error: 'States.TaskFailed' },
|
|
420
|
+
// { stateName: 'Compensate' } ]
|
|
421
|
+
console.log(simAws.stepFunctions().inspection().attempts(started.executionArn));
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Matching an error
|
|
425
|
+
|
|
426
|
+
`ErrorEquals` names the errors an entry handles. The retriers are tried first and the catchers after
|
|
427
|
+
them, each in the order it was written, and the first entry naming the error takes it. `States.ALL`
|
|
428
|
+
matches anything. Amazon States Language holds that one to an entry of its own written last, and a
|
|
429
|
+
definition breaking either rule is refused when the state machine is created.
|
|
430
|
+
|
|
431
|
+
Which name a task fails under follows the `Resource` that invoked it, as the section above
|
|
432
|
+
describes. A handler raising through `arn:aws:states:::lambda:invoke` is `States.TaskFailed`, and
|
|
433
|
+
through a function ARN it is the handler's own error type.
|
|
434
|
+
|
|
435
|
+
`States.Runtime` is left alone by both. Real Step Functions ends an execution on that one whatever a
|
|
436
|
+
`Retry` or a `Catch` names, and a catcher on `States.ALL` passes over it.
|
|
437
|
+
|
|
438
|
+
### How long a retry waits
|
|
439
|
+
|
|
440
|
+
The wait starts at `IntervalSeconds` (1 second by default) and is multiplied by `BackoffRate` (2.0)
|
|
441
|
+
for every retry already taken. `MaxDelaySeconds` caps it. `MaxAttempts` (3) counts retries. A
|
|
442
|
+
retrier left on all four defaults runs a task four times, at 0, 1, 3 and 7 seconds.
|
|
443
|
+
|
|
444
|
+
Each retrier keeps its own count, the way Amazon States Language keeps it. A task failing one way
|
|
445
|
+
and then another spends one attempt from each of the two retriers that name them. A retrier with no
|
|
446
|
+
attempts left hands the failure to the catchers, and a failure no catcher names ends the execution.
|
|
447
|
+
|
|
448
|
+
`JitterStrategy` is refused when the state machine is created. Jitter varies the wait between
|
|
449
|
+
attempts, and a test advancing a clock over that wait needs it fixed.
|
|
450
|
+
|
|
451
|
+
### Where the error lands
|
|
452
|
+
|
|
453
|
+
A catcher's `Next` names the state a caught failure goes to. Its `ResultPath` says where the error
|
|
454
|
+
output sits in that state's input, and the error output holds `Error` and `Cause` (the two the
|
|
455
|
+
example above prints). `ResultPath` reads the raw input of the state that failed. Writing it to
|
|
456
|
+
`$.error` keeps the data the task was given and puts the error beside it. A catcher carrying no
|
|
457
|
+
`ResultPath` passes the error output on by itself, and one carrying `null` passes the input on
|
|
458
|
+
untouched.
|
|
459
|
+
|
|
460
|
+
### How long a task waits
|
|
461
|
+
|
|
462
|
+
`TimeoutSeconds` and `HeartbeatSeconds` say how long a `Task` state waits for its work. A task still
|
|
463
|
+
going when simulated time reaches either one fails with `States.Timeout`, and the shorter of the two
|
|
464
|
+
fires. The failure goes to the state's `Catch` like any other. The deadline covers the state with
|
|
465
|
+
its retries in it, and a task that reaches the deadline gives up where it stands.
|
|
466
|
+
|
|
467
|
+
### Counting the attempts
|
|
468
|
+
|
|
469
|
+
`visitedStates` says which states an execution reached. `attempts` says how many runs each of those
|
|
470
|
+
states took, and what each run failed with. A test asserting that a task ran three times counts the
|
|
471
|
+
rows.
|
|
472
|
+
|
|
473
|
+
## Calling a simulated service
|
|
474
|
+
|
|
475
|
+
A `Task` state can call an operation on any service this simulation holds, through both of the forms
|
|
476
|
+
Amazon States Language gives a service integration.
|
|
477
|
+
|
|
478
|
+
`arn:aws:states:::aws-sdk:<service>:<operation>` is the SDK integration. The state's `Parameters` are
|
|
479
|
+
the request and the operation's response is its result. The service is named the way the AWS SDK
|
|
480
|
+
names it, in lower case with nothing between the words (`dynamodb`, `sqs`, `eventbridge`, `sfn`,
|
|
481
|
+
`secretsmanager`, `cloudwatchlogs`). Every simulated service answers one, and a service this
|
|
482
|
+
simulation has no simulation of is refused when the state machine is created.
|
|
483
|
+
|
|
484
|
+
Seven integrations are the ones Step Functions optimises, and each carries the request shape that
|
|
485
|
+
integration defines.
|
|
486
|
+
|
|
487
|
+
| `Resource` | Calls |
|
|
488
|
+
| ---------------------------------------- | ------------------------------- |
|
|
489
|
+
| `arn:aws:states:::dynamodb:putItem` | DynamoDB `PutItem` |
|
|
490
|
+
| `arn:aws:states:::dynamodb:getItem` | DynamoDB `GetItem` |
|
|
491
|
+
| `arn:aws:states:::dynamodb:updateItem` | DynamoDB `UpdateItem` |
|
|
492
|
+
| `arn:aws:states:::dynamodb:deleteItem` | DynamoDB `DeleteItem` |
|
|
493
|
+
| `arn:aws:states:::sns:publish` | SNS `Publish` |
|
|
494
|
+
| `arn:aws:states:::sqs:sendMessage` | SQS `SendMessage` |
|
|
495
|
+
| `arn:aws:states:::events:putEvents` | EventBridge `PutEvents` |
|
|
496
|
+
| `arn:aws:states:::states:startExecution` | Step Functions `StartExecution` |
|
|
497
|
+
|
|
498
|
+
Four of them carry a message, and an optimized integration lets that message be written as JSON.
|
|
499
|
+
`Message` on `sns:publish`, `MessageBody` on `sqs:sendMessage`, the `Detail` of each
|
|
500
|
+
`events:putEvents` entry and `Input` on `states:startExecution` are all serialised on the way out.
|
|
501
|
+
`Parameters` build a message the way they build anything else.
|
|
502
|
+
|
|
503
|
+
```typescript sim-step-functions-service-task
|
|
504
|
+
/**
|
|
505
|
+
* A workflow that records an enrolment in DynamoDB and announces it on SNS.
|
|
506
|
+
*/
|
|
507
|
+
|
|
508
|
+
import { CreateTableCommand, GetItemCommand } from "@aws-sdk/client-dynamodb";
|
|
509
|
+
import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
|
|
510
|
+
import { CreateTopicCommand } from "@aws-sdk/client-sns";
|
|
511
|
+
|
|
512
|
+
import { SimAws } from "@kensio/yulin";
|
|
513
|
+
|
|
514
|
+
const simAws = new SimAws();
|
|
515
|
+
|
|
516
|
+
await simAws.dynamoDb().createTable(
|
|
517
|
+
new CreateTableCommand({
|
|
518
|
+
TableName: "enrolments",
|
|
519
|
+
KeySchema: [{ AttributeName: "id", KeyType: "HASH" }],
|
|
520
|
+
AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }],
|
|
521
|
+
BillingMode: "PAY_PER_REQUEST",
|
|
522
|
+
}),
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
const topic = await simAws
|
|
526
|
+
.sns()
|
|
527
|
+
.createTopic(new CreateTopicCommand({ Name: "enrolments" }));
|
|
528
|
+
|
|
529
|
+
// The execution assumes this role, and every call it makes is authorized as it.
|
|
530
|
+
const role = await simAws.iam().createRole(
|
|
531
|
+
new CreateRoleCommand({
|
|
532
|
+
RoleName: "WorkflowRole",
|
|
533
|
+
AssumeRolePolicyDocument: JSON.stringify({
|
|
534
|
+
Version: "2012-10-17",
|
|
535
|
+
Statement: {
|
|
536
|
+
Effect: "Allow",
|
|
537
|
+
Principal: { Service: "states.amazonaws.com" },
|
|
538
|
+
Action: "sts:AssumeRole",
|
|
539
|
+
},
|
|
540
|
+
}),
|
|
541
|
+
}),
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
await simAws.iam().putRolePolicy(
|
|
545
|
+
new PutRolePolicyCommand({
|
|
546
|
+
RoleName: "WorkflowRole",
|
|
547
|
+
PolicyName: "RecordEnrolments",
|
|
548
|
+
PolicyDocument: JSON.stringify({
|
|
549
|
+
Version: "2012-10-17",
|
|
550
|
+
Statement: {
|
|
551
|
+
Effect: "Allow",
|
|
552
|
+
Action: ["dynamodb:PutItem", "sns:Publish"],
|
|
553
|
+
Resource: "*",
|
|
554
|
+
},
|
|
555
|
+
}),
|
|
556
|
+
}),
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
const created = await simAws.stepFunctions().createStateMachine({
|
|
560
|
+
input: {
|
|
561
|
+
name: "Enrolment",
|
|
562
|
+
roleArn: role.Role.Arn,
|
|
563
|
+
definition: JSON.stringify({
|
|
564
|
+
StartAt: "Record",
|
|
565
|
+
States: {
|
|
566
|
+
Record: {
|
|
567
|
+
Type: "Task",
|
|
568
|
+
Resource: "arn:aws:states:::dynamodb:putItem",
|
|
569
|
+
Parameters: {
|
|
570
|
+
TableName: "enrolments",
|
|
571
|
+
Item: { id: { "S.$": "$.student" }, term: { S: "2026-autumn" } },
|
|
572
|
+
},
|
|
573
|
+
// PutItem answers with nothing, and the announcement needs the input.
|
|
574
|
+
ResultPath: null,
|
|
575
|
+
Next: "Announce",
|
|
576
|
+
},
|
|
577
|
+
Announce: {
|
|
578
|
+
Type: "Task",
|
|
579
|
+
Resource: "arn:aws:states:::sns:publish",
|
|
580
|
+
Parameters: {
|
|
581
|
+
TopicArn: topic.TopicArn,
|
|
582
|
+
Message: { "student.$": "$.student", enrolled: true },
|
|
583
|
+
},
|
|
584
|
+
End: true,
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
}),
|
|
588
|
+
},
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
const started = await simAws.stepFunctions().startExecution({
|
|
592
|
+
input: {
|
|
593
|
+
stateMachineArn: created.stateMachineArn,
|
|
594
|
+
input: JSON.stringify({ student: "Wei" }),
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
const described = await simAws
|
|
599
|
+
.stepFunctions()
|
|
600
|
+
.describeExecution({ input: { executionArn: started.executionArn } });
|
|
601
|
+
|
|
602
|
+
console.log(described.status); // SUCCEEDED
|
|
603
|
+
|
|
604
|
+
const recorded = await simAws.dynamoDb().getItem(
|
|
605
|
+
new GetItemCommand({
|
|
606
|
+
TableName: "enrolments",
|
|
607
|
+
Key: { id: { S: "Wei" } },
|
|
608
|
+
}),
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
console.log(recorded.Item?.["term"]); // { S: '2026-autumn' }
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
The five data-flow fields apply around one of these calls the way they apply around any other state.
|
|
615
|
+
`Parameters` build the request, and `ResultSelector`, `ResultPath` and `OutputPath` shape the
|
|
616
|
+
response. A `Parameters` field written with `.$` reads a Reference Path or an intrinsic. The `Item`
|
|
617
|
+
above takes its key out of the execution's input that way.
|
|
618
|
+
|
|
619
|
+
The call is made in the state machine's own Account and Region. A resource in another one is reached
|
|
620
|
+
by naming it in the request, where the operation takes an ARN.
|
|
621
|
+
|
|
622
|
+
### What the execution role has to allow
|
|
623
|
+
|
|
624
|
+
Each call carries the role the execution assumed, and simulated IAM answers it against that role's
|
|
625
|
+
policies and the resource the request names. The action is the one AWS documents for the operation,
|
|
626
|
+
so `dynamodb:PutItem` for a `putItem` and `sns:Publish` for a `publish`. A role that has not been
|
|
627
|
+
granted it fails the task with an access denied error, naming the action and the resource.
|
|
628
|
+
|
|
629
|
+
`states:startExecution` is the exception. Simulated Step Functions authorizes none of its own
|
|
630
|
+
operations yet. A task starting another state machine runs whatever the role allows.
|
|
631
|
+
|
|
632
|
+
### When a service refuses a call
|
|
633
|
+
|
|
634
|
+
The task fails under the name Amazon States Language gives a service error. That name is the service
|
|
635
|
+
and the error joined by a dot. A conditional write that did not hold is
|
|
636
|
+
`DynamoDb.ConditionalCheckFailedException`, and a request the service would not accept is
|
|
637
|
+
`DynamoDb.ValidationException`. `Retry` and `Catch` match on that name.
|
|
638
|
+
|
|
639
|
+
The error is the service's own name for what it refused, and simulated IAM calls a refusal
|
|
640
|
+
`AccessDenied`. A task an execution role may not make is `DynamoDb.AccessDenied` here, where real
|
|
641
|
+
Step Functions writes `DynamoDb.AccessDeniedException`.
|
|
642
|
+
|
|
643
|
+
Anything that stopped the call from reaching the service fails the task with `States.TaskFailed`. An
|
|
644
|
+
operation this simulator has no implementation for is one of these, and the cause names the operation
|
|
645
|
+
and lists what that simulated service does run. That refusal comes when the task runs, because the
|
|
646
|
+
operations a service runs belong to the simulated service and there is none to ask while a state
|
|
647
|
+
machine is being created.
|
|
648
|
+
|
|
649
|
+
### What a Resource is refused for
|
|
650
|
+
|
|
651
|
+
`.sync` and `.waitForTaskToken` are refused when the state machine is created, naming the pattern. A
|
|
652
|
+
task here calls and answers, and both of those hold a state open until something else happens. An
|
|
653
|
+
integration for a service this simulation has no simulation of is refused the same way, naming the
|
|
654
|
+
service and the operation the definition asked for.
|
|
655
|
+
|
|
656
|
+
## Branching on the input
|
|
657
|
+
|
|
658
|
+
A `Choice` state takes the first rule its input matches, and its `Default` where none of them do.
|
|
659
|
+
|
|
660
|
+
```typescript sim-step-functions-choice
|
|
661
|
+
/**
|
|
662
|
+
* Branching on an execution's data with a Choice state.
|
|
663
|
+
*/
|
|
664
|
+
|
|
665
|
+
import { SimAws } from "@kensio/yulin";
|
|
666
|
+
|
|
667
|
+
const simAws = new SimAws();
|
|
668
|
+
|
|
669
|
+
const created = await simAws.stepFunctions().createStateMachine({
|
|
670
|
+
input: {
|
|
671
|
+
name: "Enrolment",
|
|
672
|
+
roleArn: "arn:aws:iam::123456789012:role/WorkflowRole",
|
|
673
|
+
definition: JSON.stringify({
|
|
674
|
+
StartAt: "Eligible",
|
|
675
|
+
States: {
|
|
676
|
+
Eligible: {
|
|
677
|
+
Type: "Choice",
|
|
678
|
+
Choices: [
|
|
679
|
+
{
|
|
680
|
+
And: [
|
|
681
|
+
{ Variable: "$.term", IsPresent: true },
|
|
682
|
+
{ Variable: "$.term", NumericGreaterThanEquals: 2 },
|
|
683
|
+
],
|
|
684
|
+
Next: "Enrol",
|
|
685
|
+
},
|
|
686
|
+
],
|
|
687
|
+
Default: "Decline",
|
|
688
|
+
},
|
|
689
|
+
Enrol: { Type: "Pass", Result: { enrolled: true }, End: true },
|
|
690
|
+
Decline: { Type: "Fail", Error: "NotEligible" },
|
|
691
|
+
},
|
|
692
|
+
}),
|
|
693
|
+
},
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
const started = await simAws.stepFunctions().startExecution({
|
|
697
|
+
input: {
|
|
698
|
+
stateMachineArn: created.stateMachineArn,
|
|
699
|
+
input: JSON.stringify({ student: "Wei", term: 3 }),
|
|
700
|
+
},
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
console.log(
|
|
704
|
+
simAws.stepFunctions().inspection().visitedStates(started.executionArn),
|
|
705
|
+
); // [ 'Eligible', 'Enrol' ]
|
|
706
|
+
|
|
707
|
+
const described = await simAws
|
|
708
|
+
.stepFunctions()
|
|
709
|
+
.describeExecution({ input: { executionArn: started.executionArn } });
|
|
710
|
+
|
|
711
|
+
console.log(described.output); // {"enrolled":true}
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
The comparators are the ones Amazon States Language defines. Strings compare with `StringEquals`,
|
|
715
|
+
`StringLessThan`, `StringGreaterThan`, `StringLessThanEquals`, `StringGreaterThanEquals` and
|
|
716
|
+
`StringMatches`. The same five orderings appear under `Numeric` and under `Timestamp`, and a boolean
|
|
717
|
+
compares with `BooleanEquals`. Each of those has a `Path` twin, such as `NumericGreaterThanPath`,
|
|
718
|
+
which takes a Reference Path and reads its operand from the state's own input. The data tests are
|
|
719
|
+
`IsPresent`, `IsNull`, `IsBoolean`, `IsNumeric`, `IsString` and `IsTimestamp`. Rules combine with
|
|
720
|
+
`And`, `Or` and `Not`.
|
|
721
|
+
|
|
722
|
+
`StringMatches` takes `*` as a wildcard spanning any run of characters. A backslash escapes the
|
|
723
|
+
character after it, so `Star\*` matches the literal name `Star*`. Amazon States Language gives
|
|
724
|
+
`StringMatches` no `Path` twin, and `StringMatchesPath` is refused here as a comparator it does not
|
|
725
|
+
define.
|
|
726
|
+
|
|
727
|
+
Two things fail an execution at a `Choice` state, both the way real Step Functions fails one:
|
|
728
|
+
|
|
729
|
+
- A state matching no rule with no `Default` to fall back on fails with `States.NoChoiceMatched`.
|
|
730
|
+
- A comparator whose `Variable` selects nothing fails with `States.Runtime`. Guard a field that may
|
|
731
|
+
be absent with `IsPresent` at the front of an `And`, as the example above does. The rules under an
|
|
732
|
+
`And` are tested in the order they were written and stop at the first one that fails. The data
|
|
733
|
+
tests answer for an absent field on their own.
|
|
734
|
+
|
|
735
|
+
A comparison of two different types answers false. A `StringEquals` rule tested against a number
|
|
736
|
+
falls through to the next rule. Timestamps are read as RFC3339 (`2026-07-26T09:00:00Z`), so a date
|
|
737
|
+
on its own counts as a string.
|
|
738
|
+
|
|
739
|
+
## Waiting on the clock
|
|
740
|
+
|
|
741
|
+
A `Wait` state holds the execution until an instant on the simulation's clock. Where that instant is
|
|
742
|
+
still ahead, `StartExecution` answers with the execution `RUNNING`, and moving simulated time past it
|
|
743
|
+
runs the rest of the execution.
|
|
744
|
+
|
|
745
|
+
```typescript sim-step-functions-wait
|
|
746
|
+
/**
|
|
747
|
+
* Holding an execution at a Wait state, then moving time past it.
|
|
748
|
+
*/
|
|
749
|
+
|
|
750
|
+
import { SimAws, SimFixedClock } from "@kensio/yulin";
|
|
751
|
+
|
|
752
|
+
const simAws = new SimAws({
|
|
753
|
+
clock: new SimFixedClock(new Date("2026-07-26T09:00:00.000Z")),
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
const created = await simAws.stepFunctions().createStateMachine({
|
|
757
|
+
input: {
|
|
758
|
+
name: "Enrolment",
|
|
759
|
+
roleArn: "arn:aws:iam::123456789012:role/WorkflowRole",
|
|
760
|
+
definition: JSON.stringify({
|
|
761
|
+
StartAt: "Settle",
|
|
762
|
+
States: {
|
|
763
|
+
Settle: { Type: "Wait", Seconds: 300, Next: "Confirm" },
|
|
764
|
+
Confirm: { Type: "Pass", Result: { confirmed: true }, End: true },
|
|
765
|
+
},
|
|
766
|
+
}),
|
|
767
|
+
},
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
const started = await simAws.stepFunctions().startExecution({
|
|
771
|
+
input: {
|
|
772
|
+
stateMachineArn: created.stateMachineArn,
|
|
773
|
+
input: JSON.stringify({ student: "Wei" }),
|
|
774
|
+
},
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
const waiting = await simAws
|
|
778
|
+
.stepFunctions()
|
|
779
|
+
.describeExecution({ input: { executionArn: started.executionArn } });
|
|
780
|
+
|
|
781
|
+
console.log(waiting.status); // RUNNING
|
|
782
|
+
|
|
783
|
+
await simAws.clock().advanceBy({ minutes: 6 });
|
|
784
|
+
|
|
785
|
+
const settled = await simAws
|
|
786
|
+
.stepFunctions()
|
|
787
|
+
.describeExecution({ input: { executionArn: started.executionArn } });
|
|
788
|
+
|
|
789
|
+
console.log(settled.status); // SUCCEEDED
|
|
790
|
+
console.log(settled.stopDate); // 2026-07-26T09:05:00.000Z
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
Under a frozen clock the execution stays `RUNNING` for as long as the test leaves it there.
|
|
794
|
+
Simulated time moves only when the test moves it, and a slow test holds the state it set up.
|
|
795
|
+
|
|
796
|
+
A `Wait` state carries exactly one of `Seconds`, `SecondsPath`, `Timestamp` and `TimestampPath`. A
|
|
797
|
+
wait runs from 0 to 99,999,999 seconds, which is the range Step Functions takes, and a `Timestamp` is
|
|
798
|
+
held to RFC3339 (so `2026-02-30T00:00:00Z` is refused rather than read as the second of March). The
|
|
799
|
+
two paths are read out of the state's input as it runs, and a path holding a value outside either
|
|
800
|
+
range fails the execution with `States.Runtime`. An instant already behind the clock lets the
|
|
801
|
+
execution carry straight on.
|
|
802
|
+
|
|
803
|
+
The execution stops at the instant it was waiting for, and `DescribeExecution` reports that as its
|
|
804
|
+
`stopDate`. A failure after a wait is recorded on the execution, and `advanceBy` returns as it would
|
|
805
|
+
for one that succeeded. [Simulated time](https://yulinsim.dev/time/ "Simulated time docs") covers what else
|
|
806
|
+
advancing the clock runs.
|
|
807
|
+
|
|
808
|
+
## Running branches at once
|
|
809
|
+
|
|
810
|
+
A `Parallel` state runs each of its `Branches`, and answers with an array of what they produced. The
|
|
811
|
+
array is in the order the branches were written, whatever order they finished in.
|
|
812
|
+
|
|
813
|
+
```typescript sim-step-functions-parallel
|
|
814
|
+
/**
|
|
815
|
+
* Running two branches at once with a Parallel state.
|
|
816
|
+
*/
|
|
817
|
+
|
|
818
|
+
import { SimAws, SimFixedClock } from "@kensio/yulin";
|
|
819
|
+
|
|
820
|
+
const simAws = new SimAws({
|
|
821
|
+
clock: new SimFixedClock(new Date("2026-07-26T09:00:00.000Z")),
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
const created = await simAws.stepFunctions().createStateMachine({
|
|
825
|
+
input: {
|
|
826
|
+
name: "Enrolment",
|
|
827
|
+
roleArn: "arn:aws:iam::123456789012:role/WorkflowRole",
|
|
828
|
+
definition: JSON.stringify({
|
|
829
|
+
StartAt: "Enrol",
|
|
830
|
+
States: {
|
|
831
|
+
Enrol: {
|
|
832
|
+
Type: "Parallel",
|
|
833
|
+
Branches: [
|
|
834
|
+
{
|
|
835
|
+
StartAt: "Settle",
|
|
836
|
+
States: {
|
|
837
|
+
Settle: { Type: "Wait", Seconds: 300, Next: "Register" },
|
|
838
|
+
Register: {
|
|
839
|
+
Type: "Pass",
|
|
840
|
+
Result: { registered: true },
|
|
841
|
+
End: true,
|
|
842
|
+
},
|
|
843
|
+
},
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
StartAt: "Bill",
|
|
847
|
+
States: {
|
|
848
|
+
Bill: { Type: "Pass", Result: { billed: true }, End: true },
|
|
849
|
+
},
|
|
850
|
+
},
|
|
851
|
+
],
|
|
852
|
+
Next: "Confirm",
|
|
853
|
+
},
|
|
854
|
+
Confirm: { Type: "Pass", End: true },
|
|
855
|
+
},
|
|
856
|
+
}),
|
|
857
|
+
},
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
const started = await simAws.stepFunctions().startExecution({
|
|
861
|
+
input: {
|
|
862
|
+
stateMachineArn: created.stateMachineArn,
|
|
863
|
+
input: JSON.stringify({ student: "Wei" }),
|
|
864
|
+
},
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
const waiting = await simAws
|
|
868
|
+
.stepFunctions()
|
|
869
|
+
.describeExecution({ input: { executionArn: started.executionArn } });
|
|
870
|
+
|
|
871
|
+
console.log(waiting.status); // RUNNING
|
|
872
|
+
|
|
873
|
+
await simAws.clock().advanceBy({ minutes: 6 });
|
|
874
|
+
|
|
875
|
+
const settled = await simAws
|
|
876
|
+
.stepFunctions()
|
|
877
|
+
.describeExecution({ input: { executionArn: started.executionArn } });
|
|
878
|
+
|
|
879
|
+
console.log(settled.output); // [{"registered":true},{"billed":true}]
|
|
880
|
+
|
|
881
|
+
console.log(
|
|
882
|
+
simAws
|
|
883
|
+
.stepFunctions()
|
|
884
|
+
.inspection()
|
|
885
|
+
.branches(started.executionArn)
|
|
886
|
+
.map((branch) => branch.visitedStates),
|
|
887
|
+
); // [ [ 'Settle', 'Register' ], [ 'Bill' ] ]
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
Every branch is given the state's effective input. `InputPath` and `Parameters` apply once for the
|
|
891
|
+
state, and not once per branch. `ResultSelector`, `ResultPath` and `OutputPath` then apply to the
|
|
892
|
+
array of branch outputs.
|
|
893
|
+
|
|
894
|
+
A branch is a state machine of its own, with its own `StartAt` and its own `States`. A `Next` inside
|
|
895
|
+
a branch reaches only the states in that branch, and two branches are free to use the same state
|
|
896
|
+
name. The states inside a branch are read when the state machine is created. A definition using something
|
|
897
|
+
this simulator has no implementation for inside a branch is refused there, naming the branch it was
|
|
898
|
+
written in.
|
|
899
|
+
|
|
900
|
+
A branch that reaches a `Wait` state waits on the same clock as everything else, and its siblings
|
|
901
|
+
carry on while it waits. The execution reads as `RUNNING` until the last branch has finished.
|
|
902
|
+
|
|
903
|
+
### When a branch fails
|
|
904
|
+
|
|
905
|
+
A branch that fails takes the `Parallel` state with it. The state fails with `States.BranchFailed`,
|
|
906
|
+
and the `Cause` names the branch and what it failed with:
|
|
907
|
+
|
|
908
|
+
```text
|
|
909
|
+
Branch 2 of the Parallel state Enrol failed with NotEligible: no place left on the course
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
The branches still going are given up on, along with the branches those were running themselves.
|
|
913
|
+
Whatever they had scheduled on the clock finds a branch that has stopped, so a later `advanceBy`
|
|
914
|
+
runs none of it.
|
|
915
|
+
|
|
916
|
+
`Retry` and `Catch` on the `Parallel` state itself work the way they do on a `Task` state, and are
|
|
917
|
+
written the same way. A retry runs every branch again from its own `StartAt`, on the interval and
|
|
918
|
+
backoff the retrier gives. A catcher matching `States.BranchFailed` (or `States.ALL`) sends the
|
|
919
|
+
execution to the state it names.
|
|
920
|
+
|
|
921
|
+
### Reading the branches back
|
|
922
|
+
|
|
923
|
+
The inspection accessor reports branches separately from the states around them:
|
|
924
|
+
|
|
925
|
+
```typescript
|
|
926
|
+
const branches = simAws.stepFunctions().inspection().branches(executionArn);
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
Each branch says which `Parallel` state it belongs to, where among its siblings it sits (counting
|
|
930
|
+
from zero), the states it entered and how it ended. A branch given up on because a sibling failed
|
|
931
|
+
reads as `ABANDONED`. A `Parallel` state inside a branch reports its own branches here too, however
|
|
932
|
+
deep they go. `visitedStates` on the execution holds the states outside the branches, while
|
|
933
|
+
`attempts` covers every run of every state, branches included.
|
|
934
|
+
|
|
935
|
+
## Running a state per item
|
|
936
|
+
|
|
937
|
+
A `Map` state runs its `ItemProcessor` once per item, and answers with an array of what the
|
|
938
|
+
iterations produced. The array is in the order the items were in, whatever order the iterations
|
|
939
|
+
finished in.
|
|
940
|
+
|
|
941
|
+
```typescript sim-step-functions-map
|
|
942
|
+
/**
|
|
943
|
+
* Running a state per item with a Map state.
|
|
944
|
+
*/
|
|
945
|
+
|
|
946
|
+
import { SimAws } from "@kensio/yulin";
|
|
947
|
+
|
|
948
|
+
const simAws = new SimAws();
|
|
949
|
+
|
|
950
|
+
const created = await simAws.stepFunctions().createStateMachine({
|
|
951
|
+
input: {
|
|
952
|
+
name: "Enrolment",
|
|
953
|
+
roleArn: "arn:aws:iam::123456789012:role/WorkflowRole",
|
|
954
|
+
definition: JSON.stringify({
|
|
955
|
+
StartAt: "Enrol",
|
|
956
|
+
States: {
|
|
957
|
+
Enrol: {
|
|
958
|
+
Type: "Map",
|
|
959
|
+
ItemsPath: "$.students",
|
|
960
|
+
MaxConcurrency: 2,
|
|
961
|
+
ItemSelector: {
|
|
962
|
+
"id.$": "$$.Map.Item.Value.id",
|
|
963
|
+
"at.$": "$$.Map.Item.Index",
|
|
964
|
+
"term.$": "$.term",
|
|
965
|
+
},
|
|
966
|
+
ItemProcessor: {
|
|
967
|
+
StartAt: "Register",
|
|
968
|
+
States: { Register: { Type: "Pass", End: true } },
|
|
969
|
+
},
|
|
970
|
+
End: true,
|
|
971
|
+
},
|
|
972
|
+
},
|
|
973
|
+
}),
|
|
974
|
+
},
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
const started = await simAws.stepFunctions().startExecution({
|
|
978
|
+
input: {
|
|
979
|
+
stateMachineArn: created.stateMachineArn,
|
|
980
|
+
input: JSON.stringify({
|
|
981
|
+
term: 3,
|
|
982
|
+
students: [{ id: "wei" }, { id: "mei" }],
|
|
983
|
+
}),
|
|
984
|
+
},
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
const described = await simAws
|
|
988
|
+
.stepFunctions()
|
|
989
|
+
.describeExecution({ input: { executionArn: started.executionArn } });
|
|
990
|
+
|
|
991
|
+
console.log(described.output);
|
|
992
|
+
// [{"id":"wei","at":0,"term":3},{"id":"mei","at":1,"term":3}]
|
|
993
|
+
|
|
994
|
+
console.log(
|
|
995
|
+
simAws.stepFunctions().inspection().iterations(started.executionArn).length,
|
|
996
|
+
); // 2
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
`ItemsPath` says where the items are, and a `Map` state carrying none runs over its whole effective
|
|
1000
|
+
input. What it selects has to be an array. Anything else fails the state with `States.Runtime`,
|
|
1001
|
+
which is the failure real Step Functions gives it, and no `Catch` takes that one.
|
|
1002
|
+
|
|
1003
|
+
`ItemSelector` builds what each iteration is given. It reads the `Map` state's own input through `$`
|
|
1004
|
+
and the item it is building for through `$$.Map.Item`, which holds `Value` and `Index`. The states
|
|
1005
|
+
inside the iteration read `$$.Map.Item` as well. A `Map` state carrying no `ItemSelector` gives each
|
|
1006
|
+
iteration the item itself.
|
|
1007
|
+
|
|
1008
|
+
`MaxConcurrency` bounds how many iterations run at once. A bound of 0, and a `Map` state carrying no
|
|
1009
|
+
bound at all, runs every iteration together. An iteration reaching a `Wait` state is still one of
|
|
1010
|
+
the iterations running, so a bound of 1 holds the next item back until the wait is over.
|
|
1011
|
+
|
|
1012
|
+
`Parameters` and `Iterator` are the older spellings of `ItemSelector` and `ItemProcessor`, and both
|
|
1013
|
+
are read. CDK still writes them for a `Map` built with its deprecated `parameters` property or its
|
|
1014
|
+
`iterator()` call. A state carrying both spellings of either is refused.
|
|
1015
|
+
|
|
1016
|
+
An iteration that fails fails the `Map` state, the way a branch failing fails a `Parallel` state.
|
|
1017
|
+
The state fails with `States.BranchFailed`, the iterations still going are abandoned, and the items
|
|
1018
|
+
that had not started are left alone. `Retry` and `Catch` on the `Map` state work as they do on a
|
|
1019
|
+
`Task` state, and a retry runs every iteration again.
|
|
1020
|
+
|
|
1021
|
+
Reading the iterations back is the same accessor the branches use:
|
|
1022
|
+
|
|
1023
|
+
```typescript
|
|
1024
|
+
const iterations = simAws.stepFunctions().inspection().iterations(executionArn);
|
|
1025
|
+
```
|
|
1026
|
+
|
|
1027
|
+
## Through an intercepted SDK client
|
|
1028
|
+
|
|
1029
|
+
An `SFNClient` handed to `SimSdk` reaches the same simulated service:
|
|
1030
|
+
|
|
1031
|
+
```typescript sim-step-functions-sdk
|
|
1032
|
+
/**
|
|
1033
|
+
* Running a state machine through an intercepted SFNClient.
|
|
1034
|
+
*/
|
|
1035
|
+
|
|
1036
|
+
import {
|
|
1037
|
+
CreateStateMachineCommand,
|
|
1038
|
+
SFNClient,
|
|
1039
|
+
StartExecutionCommand,
|
|
1040
|
+
} from "@aws-sdk/client-sfn";
|
|
1041
|
+
|
|
1042
|
+
import { SimAws } from "@kensio/yulin";
|
|
1043
|
+
import { SimSdk } from "@kensio/yulin/sdk";
|
|
1044
|
+
|
|
1045
|
+
const simAws = new SimAws();
|
|
1046
|
+
const client = new SFNClient({ region: simAws.defaultRegionName });
|
|
1047
|
+
|
|
1048
|
+
using _intercepted = new SimSdk({ simAws }).intercept(client);
|
|
1049
|
+
|
|
1050
|
+
const created = await client.send(
|
|
1051
|
+
new CreateStateMachineCommand({
|
|
1052
|
+
name: "Enrolment",
|
|
1053
|
+
roleArn: "arn:aws:iam::123456789012:role/WorkflowRole",
|
|
1054
|
+
definition: JSON.stringify({
|
|
1055
|
+
StartAt: "Done",
|
|
1056
|
+
States: { Done: { Type: "Succeed" } },
|
|
1057
|
+
}),
|
|
1058
|
+
}),
|
|
1059
|
+
);
|
|
1060
|
+
|
|
1061
|
+
const started = await client.send(
|
|
1062
|
+
new StartExecutionCommand({ stateMachineArn: created.stateMachineArn }),
|
|
1063
|
+
);
|
|
1064
|
+
|
|
1065
|
+
console.log(started.executionArn);
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1068
|
+
## Tagging a state machine
|
|
1069
|
+
|
|
1070
|
+
`CreateStateMachine` takes tags, and `TagResource`, `UntagResource` and `ListTagsForResource` read
|
|
1071
|
+
and write them afterwards. A tag is a `key` and a `value`, both lower case, as the Step Functions
|
|
1072
|
+
API writes them.
|
|
1073
|
+
|
|
1074
|
+
```typescript sim-step-functions-tags
|
|
1075
|
+
/**
|
|
1076
|
+
* Tagging a state machine and reading its tags back.
|
|
1077
|
+
*/
|
|
1078
|
+
|
|
1079
|
+
import { SimAws } from "@kensio/yulin";
|
|
1080
|
+
|
|
1081
|
+
const simAws = new SimAws();
|
|
1082
|
+
|
|
1083
|
+
const created = await simAws.stepFunctions().createStateMachine({
|
|
1084
|
+
input: {
|
|
1085
|
+
name: "Enrolment",
|
|
1086
|
+
roleArn: "arn:aws:iam::123456789012:role/WorkflowRole",
|
|
1087
|
+
definition: JSON.stringify({
|
|
1088
|
+
StartAt: "Done",
|
|
1089
|
+
States: { Done: { Type: "Succeed" } },
|
|
1090
|
+
}),
|
|
1091
|
+
tags: [{ key: "team", value: "enrolment" }],
|
|
1092
|
+
},
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
await simAws.stepFunctions().tagResource({
|
|
1096
|
+
input: {
|
|
1097
|
+
resourceArn: created.stateMachineArn,
|
|
1098
|
+
tags: [{ key: "term", value: "autumn" }],
|
|
1099
|
+
},
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
await simAws.stepFunctions().untagResource({
|
|
1103
|
+
input: { resourceArn: created.stateMachineArn, tagKeys: ["team"] },
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
const listed = await simAws
|
|
1107
|
+
.stepFunctions()
|
|
1108
|
+
.listTagsForResource({ input: { resourceArn: created.stateMachineArn } });
|
|
1109
|
+
|
|
1110
|
+
console.log(listed.tags); // [ { key: 'term', value: 'autumn' } ]
|
|
1111
|
+
```
|
|
1112
|
+
|
|
1113
|
+
A key is held once. `TagResource` adds a new key, and replaces the value of a key already held.
|
|
1114
|
+
`UntagResource` takes off the keys it names, and passes over a key that was never there.
|
|
1115
|
+
|
|
1116
|
+
A key runs to 128 characters and a value to 256, and one resource holds 50 tags. A value may be
|
|
1117
|
+
empty, where a key may not. Letters, digits, whitespace and `+ - = . _ : / @` are what a tag is
|
|
1118
|
+
written with. A key or a value beginning `aws:` is refused, since AWS assigns tags of its own under
|
|
1119
|
+
that prefix. A request outside any of those limits leaves the tags exactly as they were.
|
|
1120
|
+
|
|
1121
|
+
`TagResource` requires `tags` and `UntagResource` requires `tagKeys`, as the API does. Either list
|
|
1122
|
+
may be empty. A request that omits one is refused before the state machine is looked up, along with
|
|
1123
|
+
a request carrying a tag Step Functions will not take.
|
|
1124
|
+
|
|
1125
|
+
## Deploying one from CloudFormation
|
|
1126
|
+
|
|
1127
|
+
A template declaring `AWS::StepFunctions::StateMachine` creates a state machine. It goes through the
|
|
1128
|
+
ordinary `CreateStateMachine` command, and a template and an SDK caller reach the same state machine.
|
|
1129
|
+
A definition Amazon States Language itself refuses fails the Resource, in the words
|
|
1130
|
+
`CreateStateMachine` refuses it in. A definition this simulator has no implementation for takes the
|
|
1131
|
+
skip described at the end of this section.
|
|
1132
|
+
|
|
1133
|
+
CDK synthesizes the Resource from `stepfunctions.StateMachine`. Deploy the synthesized assembly and
|
|
1134
|
+
the workflow is there to start an execution against.
|
|
1135
|
+
|
|
1136
|
+
```typescript sim-step-functions-cloudformation
|
|
1137
|
+
/**
|
|
1138
|
+
* Running an execution against a state machine a CDK app deployed.
|
|
1139
|
+
*/
|
|
1140
|
+
|
|
1141
|
+
import path from "node:path";
|
|
1142
|
+
|
|
1143
|
+
import { SimAws } from "@kensio/yulin";
|
|
1144
|
+
|
|
1145
|
+
const simAws = new SimAws({ defaultRegionName: "eu-west-2" });
|
|
1146
|
+
|
|
1147
|
+
// The CDK app holds `new sfn.StateMachine(stack, "Workflow", {
|
|
1148
|
+
// stateMachineName: "Enrolment",
|
|
1149
|
+
// definitionBody: sfn.DefinitionBody.fromChainable(record.next(done)),
|
|
1150
|
+
// })`.
|
|
1151
|
+
await simAws.cloudFormation().deployCdkOut(path.join(process.cwd(), "cdk.out"));
|
|
1152
|
+
|
|
1153
|
+
const workflow = simAws.stepFunctions().findStateMachine("Enrolment");
|
|
1154
|
+
|
|
1155
|
+
if (workflow === undefined) throw new Error("No Enrolment state machine");
|
|
1156
|
+
|
|
1157
|
+
const started = await simAws.stepFunctions().startExecution({
|
|
1158
|
+
input: {
|
|
1159
|
+
stateMachineArn: workflow.arn,
|
|
1160
|
+
input: JSON.stringify({ student: "Wei" }),
|
|
1161
|
+
},
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
const described = await simAws
|
|
1165
|
+
.stepFunctions()
|
|
1166
|
+
.describeExecution({ input: { executionArn: started.executionArn } });
|
|
1167
|
+
|
|
1168
|
+
console.log(described.status); // SUCCEEDED
|
|
1169
|
+
```
|
|
1170
|
+
|
|
1171
|
+
The definition is read once the template intrinsics have resolved. CDK writes `DefinitionString` as
|
|
1172
|
+
an `Fn::Join` over the ARNs of the resources the workflow reaches, and the joined string is the
|
|
1173
|
+
Amazon States Language the interpreter reads. A template can write the same document as template
|
|
1174
|
+
data under `Definition`. `DefinitionSubstitutions` replaces every `${Key}` in the definition before
|
|
1175
|
+
it is read.
|
|
1176
|
+
|
|
1177
|
+
`DefinitionS3Location` names an object holding the definition. CDK writes that form for
|
|
1178
|
+
`DefinitionBody.fromFile`, and for a definition past the template size limit. The object is fetched
|
|
1179
|
+
from simulated S3, where the CDK assets publisher put the staged file before any Resource was
|
|
1180
|
+
created. A location this simulation holds no object for drops that one state machine and records
|
|
1181
|
+
where it looked.
|
|
1182
|
+
|
|
1183
|
+
`StateMachineName`, `RoleArn`, `StateMachineType` and `Tags` are carried across. A state machine the
|
|
1184
|
+
template does not name is named after the stack and the logical ID (`enrolment-Workflow`), the way
|
|
1185
|
+
CloudFormation names one. `Ref` answers with the ARN and `Fn::GetAtt` answers `Arn` and `Name`. Real
|
|
1186
|
+
CloudFormation publishes this one that way round too. Deleting the stack deletes the state machine.
|
|
1187
|
+
|
|
1188
|
+
A definition holding a state type this simulator does not run drops that one state machine. The
|
|
1189
|
+
reason lands on `stack.skippedResources` and the rest of the stack deploys. The whole state machine
|
|
1190
|
+
goes. A state machine missing one state runs wrong, and a test watching it run wrong is worse off
|
|
1191
|
+
than a test watching it be absent.
|
|
1192
|
+
|
|
1193
|
+
CDK's `LambdaInvoke` gives its task a `Retry` over the Lambda service errors. `Retry` is
|
|
1194
|
+
unsimulated, and a state machine carrying one takes that same skip. Pass
|
|
1195
|
+
`retryOnServiceExceptions: false` to the task for a workflow that deploys here.
|
|
1196
|
+
|
|
1197
|
+
## Reference Paths
|
|
1198
|
+
|
|
1199
|
+
The path subset read here is the one Amazon States Language itself uses. A document root, a child by
|
|
1200
|
+
dot or by bracketed name, and an array element by index. `$.abc.['def ghi']` works, which is how the
|
|
1201
|
+
Amazon States Language docs write a field name holding a space.
|
|
1202
|
+
|
|
1203
|
+
The wider JSONPath grammar is refused by name. Wildcards, filters, slices and recursive descent all
|
|
1204
|
+
raise. A path that would have selected the wrong node fails, and no state is answered with plausible
|
|
1205
|
+
data. A dotted field name is held to the JsonPath `member-name-shorthand` rule. `$.a-b` is refused,
|
|
1206
|
+
and `$['a-b']` is the way to write it.
|
|
1207
|
+
|
|
1208
|
+
A path rooted at `$$` reads the context object rather than the state's data. See below.
|
|
1209
|
+
|
|
1210
|
+
## The context object
|
|
1211
|
+
|
|
1212
|
+
`$$` reads what the execution knows about itself. It is read in `InputPath`, `OutputPath`,
|
|
1213
|
+
`ItemsPath`, the Payload Template fields (`Parameters`, `ResultSelector` and `ItemSelector`) and an
|
|
1214
|
+
intrinsic function's arguments.
|
|
1215
|
+
|
|
1216
|
+
```json
|
|
1217
|
+
{
|
|
1218
|
+
"Execution": {
|
|
1219
|
+
"Id": "arn:aws:states:eu-west-2:123456789012:execution:Enrolment:execution-1",
|
|
1220
|
+
"Input": { "student": "Wei" },
|
|
1221
|
+
"Name": "execution-1",
|
|
1222
|
+
"RoleArn": "arn:aws:iam::123456789012:role/WorkflowRole",
|
|
1223
|
+
"StartTime": "2026-07-26T09:00:00.000Z"
|
|
1224
|
+
},
|
|
1225
|
+
"StateMachine": {
|
|
1226
|
+
"Id": "arn:aws:states:eu-west-2:123456789012:stateMachine:Enrolment",
|
|
1227
|
+
"Name": "Enrolment"
|
|
1228
|
+
},
|
|
1229
|
+
"State": {
|
|
1230
|
+
"EnteredTime": "2026-07-26T09:00:00.000Z",
|
|
1231
|
+
"Name": "Check",
|
|
1232
|
+
"RetryCount": 0
|
|
1233
|
+
},
|
|
1234
|
+
"Map": { "Item": { "Index": 0, "Value": { "id": "wei" } } }
|
|
1235
|
+
}
|
|
1236
|
+
```
|
|
1237
|
+
|
|
1238
|
+
`State` is the state now running, and `RetryCount` counts the retries this entry to it has taken.
|
|
1239
|
+
`Map` is there inside a `Map` state's `ItemSelector` and inside the iteration it built.
|
|
1240
|
+
|
|
1241
|
+
`$$.Task.Token` is unsimulated, along with the task tokens it belongs to. A path reading it selects
|
|
1242
|
+
nothing and fails the state that read it. A field that writes, such as `ResultPath`, takes a path
|
|
1243
|
+
rooted at `$`, since the context object is read rather than written.
|
|
1244
|
+
|
|
1245
|
+
## Intrinsic functions
|
|
1246
|
+
|
|
1247
|
+
`States.Format`, `States.Array`, `States.ArrayLength`, `States.StringToJson` and
|
|
1248
|
+
`States.JsonToString` all run, including calls nested inside one another.
|
|
1249
|
+
|
|
1250
|
+
`States.UUID` and `States.MathRandom` are left out on purpose. Both answer differently on every call.
|
|
1251
|
+
A test asserting on the output of a state machine that used one could only assert on its shape.
|
|
1252
|
+
|
|
1253
|
+
## Where this differs from real Step Functions
|
|
1254
|
+
|
|
1255
|
+
- **`StartExecution` runs the execution as far as it goes before it answers.** Real Step Functions
|
|
1256
|
+
answers before the execution has run, and a caller there sees `RUNNING` first. An execution here
|
|
1257
|
+
with nothing to wait for has finished by the time the caller reads it back, and one held at a
|
|
1258
|
+
`Wait` state reads as `RUNNING`. That spares every test a wait for work that is already done.
|
|
1259
|
+
- **An `EXPRESS` state machine runs the standard way.** The type is carried and read back, and
|
|
1260
|
+
`StartSyncExecution` is unsimulated.
|
|
1261
|
+
- **An unnamed execution is named by a counter.** Real Step Functions uses a UUID. A counter means a
|
|
1262
|
+
simulation answers the same way twice. The counter steps over any name a caller has already used.
|
|
1263
|
+
- **A name is taken for good.** Real Step Functions frees an execution name 90 days after the
|
|
1264
|
+
execution closes. A name here stays taken for the life of the simulation, which no test runs long
|
|
1265
|
+
enough to notice. `StartExecution` is idempotent while an execution is still running, as it is on
|
|
1266
|
+
AWS. A repeat carrying the same name and input answers with the execution already there, and one
|
|
1267
|
+
carrying different input raises `ExecutionAlreadyExists`.
|
|
1268
|
+
- **A brace escape outside `States.Format` keeps its backslash.** A brace is a placeholder to
|
|
1269
|
+
`States.Format` alone, so `States.Format` is where `\{` is resolved. An escaped brace reaching
|
|
1270
|
+
another intrinsic arrives as it was written.
|
|
1271
|
+
- **`GetExecutionHistory` is unsimulated.** The inspection accessor answers the same question.
|
|
1272
|
+
- **A task's `Invoke` response carries three fields.** `ExecutedVersion`, `Payload` and
|
|
1273
|
+
`StatusCode`. Real Step Functions adds `SdkHttpMetadata` and `SdkResponseMetadata`, which describe
|
|
1274
|
+
a call over a network there was none of here.
|
|
1275
|
+
- **A service call an execution role may not make fails as `AccessDenied`.** Simulated IAM names a
|
|
1276
|
+
refusal that way, and the task error is the service and that name joined by a dot. Real Step
|
|
1277
|
+
Functions writes the service's own `AccessDeniedException`.
|
|
1278
|
+
- **An operation a simulated service has no implementation for is refused when the task runs.** The
|
|
1279
|
+
operations a service runs belong to the simulated service, and there is none to ask while the
|
|
1280
|
+
state machine is being created. The service itself is refused there.
|
|
1281
|
+
- **A failed task's `Cause` is the handler's message.** Real Step Functions writes the JSON error
|
|
1282
|
+
document Lambda answered with, holding `errorMessage`, `errorType` and a stack trace.
|
|
1283
|
+
- **A task's timeout covers the whole state.** Real Step Functions gives every attempt its own
|
|
1284
|
+
`TimeoutSeconds`. An attempt here takes no simulated time by itself, and a per-attempt deadline
|
|
1285
|
+
would never be reached. The deadline runs from the instant the execution entered the state, and a
|
|
1286
|
+
task that reaches it goes to its `Catch` without another attempt.
|
|
1287
|
+
- **Nothing sends a heartbeat.** `HeartbeatSeconds` behaves as a second, shorter `TimeoutSeconds`.
|
|
1288
|
+
Activities and task tokens are unsimulated, and there is no worker to send one from.
|
|
1289
|
+
- **`TimeoutSecondsPath` and `HeartbeatSecondsPath` are unsimulated.** The two literal fields say
|
|
1290
|
+
the same thing in the definition. A `Task` state carrying either path is refused when the state
|
|
1291
|
+
machine is created.
|
|
1292
|
+
- **A cycle in the states fails the execution** after 25,000 transitions, with `States.Runtime`. Real
|
|
1293
|
+
Step Functions stops one when it runs out of execution history events.
|
|
1294
|
+
- **A branch that failed on the data it was given keeps `States.Runtime`.** Every other branch
|
|
1295
|
+
failure becomes `States.BranchFailed` on the `Parallel` state. `States.Runtime` is the one error
|
|
1296
|
+
nothing catches, and a state around a branch is no more able to carry on than the branch was.
|
|
1297
|
+
- **A branch's own states are reported apart from the execution's.** Real Step Functions writes
|
|
1298
|
+
them into one execution history, under events naming the branch. The inspection accessor answers
|
|
1299
|
+
`branches` and `iterations` instead, since a test asserting on a branch is asking about that
|
|
1300
|
+
branch.
|
|
1301
|
+
- **A Distributed Map is refused.** `ItemReader`, `ResultWriter`, `ItemBatcher`, the two
|
|
1302
|
+
`ToleratedFailure` fields and a `ProcessorConfig` asking for `DISTRIBUTED` are all refused when
|
|
1303
|
+
the state machine is created. A Distributed Map reads its items from S3 and runs a child
|
|
1304
|
+
execution per batch, which is a second execution model rather than a field or two on this one.
|
|
1305
|
+
- **A `Map` state's iterations that had not started are left alone when one fails.** The ones
|
|
1306
|
+
running are abandoned, and `inspection().iterations()` reports only the iterations that ran. Real
|
|
1307
|
+
Step Functions stops the same work, and its execution history says as much.
|
|
1308
|
+
|
|
1309
|
+
- **A state machine is the only resource that holds tags.** An activity and an execution both take
|
|
1310
|
+
tags on real Step Functions, and both are unsimulated here. A tag request naming one is refused
|
|
1311
|
+
as an ARN this holds nothing under.
|
|
1312
|
+
- **A `Version` on `DefinitionS3Location` is read past.** Simulated S3 holds one body per key, and a
|
|
1313
|
+
template naming a version gets that body.
|
|
1314
|
+
- **`LoggingConfiguration`, `TracingConfiguration` and `EncryptionConfiguration` are recorded on
|
|
1315
|
+
`stack.ignoredProperties`.** An execution writes no log events, X-Ray is unsimulated, and a
|
|
1316
|
+
definition is held as it was written, with a key asked for nowhere.
|
|
1317
|
+
- **`AWS::StepFunctions::StateMachineVersion` and `AWS::StepFunctions::StateMachineAlias` are
|
|
1318
|
+
unsupported.** Every execution runs the definition the state machine currently holds, so there is
|
|
1319
|
+
nothing for a published version or an alias to point at.
|
|
1320
|
+
|
|
1321
|
+
`CreateStateMachine` is idempotent, as it is on AWS. A second request carrying the same name,
|
|
1322
|
+
definition and type answers with the state machine already there, and one carrying a different
|
|
1323
|
+
definition raises `StateMachineAlreadyExists`. A differing `roleArn` is ignored. The AWS API
|
|
1324
|
+
reference contradicts itself here, listing a differing role ARN under `StateMachineAlreadyExists`
|
|
1325
|
+
while the operation's own note says the difference is ignored. The note is the more specific of the
|
|
1326
|
+
two and is what this follows.
|
|
1327
|
+
|
|
1328
|
+
## Still to come
|
|
1329
|
+
|
|
1330
|
+
- Distributed Map, with `ItemReader`, `ResultWriter` and the `ToleratedFailure` fields.
|
|
1331
|
+
- The `.sync` pattern, task tokens and activities.
|
|
1332
|
+
- JSONata as a query language, and the `Assign` variables that go with it.
|