@kensio/yulin-aws-simulation 1.3.0 → 1.6.0
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/.claude-plugin/plugin.json +1 -1
- package/README.md +17 -17
- package/package.json +1 -1
- package/skills/yulin-aws-simulation/SKILL.md +47 -47
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "yulin-aws-simulation",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.6.0",
|
|
5
5
|
"description": "How to test AWS code well with the @kensio/yulin in-process simulator: using it directly instead of building a harness around it, driving tests, local dev and production from one synthesized CDK template, intercepting SDK clients, controlling simulated time, and handling what the simulator refuses.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Kensio Software",
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
A Claude Code skill for testing AWS code with [Yulin](https://yulinsim.dev/) (`@kensio/yulin`), an
|
|
4
4
|
AWS simulator that runs in process, in memory, with no network and no AWS account.
|
|
5
5
|
|
|
6
|
-
Yulin's own docs are the authority on its API. This skill is the usage guidance
|
|
6
|
+
Yulin's own docs are the authority on its API. This skill is the usage guidance missing from the
|
|
7
7
|
API: what to reach for, what to avoid, and what to do when the simulator refuses something.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
@@ -24,11 +24,11 @@ npm install @kensio/yulin-aws-simulation
|
|
|
24
24
|
## What it covers
|
|
25
25
|
|
|
26
26
|
**Deploy your real synthesized template.** Deploy the JSON CDK produced, with
|
|
27
|
-
`deployTemplateFile({ templatePath, stackName })
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
`deployTemplateFile({ templatePath, stackName })`. A construct change that breaks the system breaks
|
|
28
|
+
the test. Do not hand-roll a wrapper that reads the file and calls `deployTemplate`: the file path
|
|
29
|
+
is how Yulin finds the cloud assembly beside it. A wrapper loses staged CDK assets. `transform`
|
|
30
|
+
handles what a simulation cannot resolve, such as an ARN carrying a real account or a hosted zone ID
|
|
31
|
+
from a CDK lookup, and `watch` re-applies the file on change for dev servers.
|
|
32
32
|
|
|
33
33
|
**Intercept real SDK clients with `SimSdk`, never hand-roll stubs.**
|
|
34
34
|
`simSdk.intercept(SecretsManagerClient)` makes real clients answer from the simulation, and the code
|
|
@@ -39,30 +39,30 @@ already reached production. It caught a wrongly computed Cognito `SECRET_HASH` t
|
|
|
39
39
|
accepted.
|
|
40
40
|
|
|
41
41
|
**Match service errors by `name`, not `instanceof`.** The SDK exports its exception classes, which
|
|
42
|
-
invites the wrong check. `instanceof` holds only while exactly one copy of the SDK is in play
|
|
42
|
+
invites the wrong check. `instanceof` holds only while exactly one copy of the SDK is in play. It
|
|
43
43
|
passes in production and fails against the simulator. `name` is what the wire carries, and is the
|
|
44
44
|
check that is right in both places.
|
|
45
45
|
|
|
46
|
-
**Expect refusals, and treat them as a feature.** Yulin refuses a property it
|
|
47
|
-
|
|
48
|
-
The cost is that one unsupported setting can make a whole stack unsimulatable, so enumerate
|
|
49
|
-
refusal in one pass
|
|
50
|
-
them together.
|
|
46
|
+
**Expect refusals, and treat them as a feature.** Yulin refuses a property it cannot simulate, and
|
|
47
|
+
never quietly ignores one, because silently accepting something that changes real behaviour is
|
|
48
|
+
worse. The cost is that one unsupported setting can make a whole stack unsimulatable, so enumerate
|
|
49
|
+
every refusal in one pass. Strip properties from the synthesized template until it deploys, then
|
|
50
|
+
raise them together.
|
|
51
51
|
|
|
52
52
|
**Raise gaps upstream, and weight false passes far above false refusals.** A simulator that stays
|
|
53
|
-
silent about something costs
|
|
54
|
-
deploy-time failure into a production one
|
|
53
|
+
silent about something costs you little. One that says 200 where production says 403 converts a
|
|
54
|
+
deploy-time failure into a production one. That is the opposite of what it is for. Yulin once
|
|
55
55
|
authorised a Lambda function URL invocation against `lambda:InvokeFunctionUrl` alone, where
|
|
56
56
|
CloudFront origin access control also needs `lambda:InvokeFunction`. The tests passed, the release
|
|
57
57
|
went out, and the endpoint 403'd in production.
|
|
58
58
|
|
|
59
|
-
**Deploy expensive context once per test file.** Vitest gives each file its own worker
|
|
59
|
+
**Deploy expensive context once per test file.** Vitest gives each file its own worker. A stack
|
|
60
60
|
deployed in `beforeAll` is already isolated between files. Isolation inside the file comes from
|
|
61
61
|
randomised names.
|
|
62
62
|
|
|
63
63
|
**Run the handler as a real simulated Lambda.** Bind an in-process handler to a template function
|
|
64
|
-
and its SDK calls are routed into the simulation as the execution role
|
|
65
|
-
|
|
64
|
+
and its SDK calls are routed into the simulation as the execution role. A missing permission on that
|
|
65
|
+
role fails the test at the point AWS would have failed it.
|
|
66
66
|
|
|
67
67
|
## Related skills
|
|
68
68
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kensio/yulin-aws-simulation",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "How to test AWS code well with the @kensio/yulin in-process simulator: using it directly instead of building a harness around it, driving tests, local dev and production from one synthesized CDK template, intercepting SDK clients, controlling simulated time, and handling what the simulator refuses.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"aws",
|
|
@@ -7,43 +7,43 @@ description: Use the @kensio/yulin in-process AWS simulator well when testing AW
|
|
|
7
7
|
|
|
8
8
|
[Yulin](https://yulinsim.dev/) (`@kensio/yulin`) simulates AWS in process, in memory, with no
|
|
9
9
|
network and no AWS account. Its own docs are the authority on the API. This skill covers how to use
|
|
10
|
-
it well,
|
|
10
|
+
it well, and that lives mostly outside the API.
|
|
11
11
|
|
|
12
12
|
Read the package docs for anything API-shaped:
|
|
13
13
|
[the README](https://github.com/KensioSoftware/yulin#readme), `docs/sdk/` for interception, and
|
|
14
14
|
`docs/services/<name>/` for each simulated service.
|
|
15
15
|
|
|
16
|
-
This skill serves the `isolated-testing-style` skill,
|
|
17
|
-
|
|
16
|
+
This skill serves the `isolated-testing-style` skill, the general argument for simulation over
|
|
17
|
+
stubs.
|
|
18
18
|
|
|
19
19
|
Yulin is deliberately flexible, and plenty of shapes work. What follows is the recommended way to
|
|
20
|
-
get the most out of it
|
|
21
|
-
|
|
20
|
+
get the most out of it, not a set of rules. Each one says what it buys. A situation that does not
|
|
21
|
+
want that trade can go the other way knowingly.
|
|
22
22
|
|
|
23
23
|
## Use what Yulin already gives you
|
|
24
24
|
|
|
25
25
|
The most common way to go wrong with Yulin is to build something on top of it. The failure looks
|
|
26
26
|
like a `TestAwsEnvironment` class, a `setupSimulatedAws()` helper returning six things, a factory
|
|
27
|
-
per service, or a `beforeEach` that reassembles the world
|
|
28
|
-
|
|
27
|
+
per service, or a `beforeEach` that reassembles the world (a private framework wrapped around a tool
|
|
28
|
+
that is already the framework.
|
|
29
29
|
|
|
30
30
|
It is worth resisting, because Yulin is built to be used directly:
|
|
31
31
|
|
|
32
|
-
- `new SimAws()` and `new SimSdk()` are plain constructors. No side effects, no network,
|
|
33
|
-
|
|
32
|
+
- `new SimAws()` and `new SimSdk()` are plain constructors. No side effects, no network, no cleanup
|
|
33
|
+
and no awaiting. Creating a simulation per test is close to free.
|
|
34
34
|
- Service accessors take the same Command objects the AWS SDK does, so seeding and asserting need no
|
|
35
35
|
translation layer of their own.
|
|
36
36
|
- `using simSdk = new SimSdk()` is the teardown.
|
|
37
37
|
- `deployTemplateFile` is the environment.
|
|
38
38
|
|
|
39
|
-
So the recommended shape of a test is
|
|
39
|
+
So the recommended shape of a test is to construct, deploy if a template is involved, intercept,
|
|
40
40
|
exercise, then assert by reading the simulation back. A wrapper around any of those steps hides the
|
|
41
41
|
one thing a reader of the test needs to see, and it has to be maintained forever after.
|
|
42
42
|
|
|
43
43
|
If a sequence genuinely repeats, make it a small function in the same test file, and keep it
|
|
44
|
-
returning the simulation objects themselves
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
returning the simulation objects themselves, not a bespoke shape of its own. The point at which it
|
|
45
|
+
wants a class, an options interface, or a directory, it has stopped being test setup and become a
|
|
46
|
+
second product.
|
|
47
47
|
|
|
48
48
|
## One synthesized template, for tests, local dev and production
|
|
49
49
|
|
|
@@ -65,26 +65,26 @@ Deploying the synthesized output means a construct change that breaks the system
|
|
|
65
65
|
The same argument extends past the test suite. A dev environment that creates its buckets and tables
|
|
66
66
|
by hand is a third description of the infrastructure, drifting away from the other two at its own
|
|
67
67
|
pace, and the drift shows up as a bug that reproduces in exactly one of the three places. Pointing
|
|
68
|
-
the dev server at `cdk.out` as well removes the whole category
|
|
68
|
+
the dev server at `cdk.out` as well removes the whole category. What runs locally is what CI tested
|
|
69
69
|
and what production deploys, and `watch` turns a `cdk synth` into a stack update in place.
|
|
70
70
|
|
|
71
71
|
The corollary is that infrastructure belongs in the CDK app even when only a test needs it. A bucket
|
|
72
72
|
conjured in test setup is infrastructure that production does not have.
|
|
73
73
|
|
|
74
74
|
**Do not hand-roll a wrapper that reads the file and calls `deployTemplate`.** `deployTemplateFile`
|
|
75
|
-
already reads it, and it locates the cloud assembly beside the file
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
already reads it, and it locates the cloud assembly beside the file. The assets manifest and staged
|
|
76
|
+
asset directories resolve. A wrapper that reads the JSON itself loses that, and anything needing a
|
|
77
|
+
CDK asset, such as a `Custom::CDKBucketDeployment` or a `Code.fromAsset` function, fails with
|
|
78
|
+
`No CDK assets manifest is available.`
|
|
79
79
|
|
|
80
80
|
The two options that make a wrapper unnecessary:
|
|
81
81
|
|
|
82
82
|
- **`transform`** is given the parsed template and answers with the one to deploy. It runs on the
|
|
83
|
-
deployment and again on every re-read
|
|
84
|
-
|
|
83
|
+
deployment and again on every re-read. A wrapper cannot do that. Use it for what a simulation
|
|
84
|
+
genuinely cannot resolve, such as an ARN carrying a real account, or a hosted zone ID that came
|
|
85
85
|
from `HostedZone.fromLookup`.
|
|
86
86
|
- **`watch`** re-applies the file when it changes, updating the stack in place. This is for dev
|
|
87
|
-
servers
|
|
87
|
+
servers. A `cdk synth` becomes a stack update without restarting the process, and resources the
|
|
88
88
|
change left alone keep what they hold.
|
|
89
89
|
|
|
90
90
|
```typescript
|
|
@@ -113,24 +113,24 @@ A stub asserts that your code called something. The simulator asserts that it ca
|
|
|
113
113
|
correctly. On a real project, swapping stubs for interception caught two bugs the same afternoon,
|
|
114
114
|
both already in production:
|
|
115
115
|
|
|
116
|
-
- A Secrets Manager secret whose name ended in a hyphen and six characters,
|
|
117
|
-
|
|
118
|
-
ambiguous with the ARN form. A stub has no naming rules
|
|
116
|
+
- A Secrets Manager secret whose name ended in a hyphen and six characters, exactly the suffix
|
|
117
|
+
Secrets Manager appends to an ARN. AWS advises against names of that shape because they are
|
|
118
|
+
ambiguous with the ARN form. A stub has no naming rules. It had accepted it happily.
|
|
119
119
|
- A Cognito `SECRET_HASH` computed the wrong way. The stub had accepted that too, because a stub is
|
|
120
120
|
never going to verify a signature.
|
|
121
121
|
|
|
122
|
-
Intercept the class
|
|
123
|
-
|
|
122
|
+
Intercept the class, not the instance, in most cases, since the code under test usually constructs
|
|
123
|
+
its own clients. Intercept an instance when only one client should reach the simulation.
|
|
124
124
|
|
|
125
125
|
Each `SimSdk` owns a `SimAws`, reachable as `simSdk.simAws` for seeding and inspecting state. Pass
|
|
126
126
|
an existing one with `new SimSdk({ simAws })` to share.
|
|
127
127
|
|
|
128
128
|
### Intercept what the code actually sends through
|
|
129
129
|
|
|
130
|
-
Interception replaces `send` on the thing it is given
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
Interception replaces `send` on the thing it is given. It has to be given the client the code under
|
|
131
|
+
test actually calls. The wrapper clients are where this bites. A `DynamoDBDocumentClient` built over
|
|
132
|
+
a `DynamoDBClient` is what the code sends through. It is the document client that needs
|
|
133
|
+
intercepting, not the client underneath it.
|
|
134
134
|
|
|
135
135
|
```typescript
|
|
136
136
|
using simSdk = new SimSdk();
|
|
@@ -140,7 +140,7 @@ simSdk.intercept(documents); // Not the DynamoDBClient it was built from.
|
|
|
140
140
|
```
|
|
141
141
|
|
|
142
142
|
Every Command through an intercepted client is routed to the simulation by default. An allow list of
|
|
143
|
-
Command classes narrows that,
|
|
143
|
+
Command classes narrows that, and is worth reaching for only when something else should genuinely
|
|
144
144
|
handle the rest.
|
|
145
145
|
|
|
146
146
|
### Prefer `using` over a teardown step
|
|
@@ -172,20 +172,20 @@ microseconds rather than something it waits for or gives up on. A good deal of t
|
|
|
172
172
|
off that clock: EventBridge rules and Scheduler schedules fire only when time is advanced past them,
|
|
173
173
|
DynamoDB items pass their TTL, Secrets Manager deletions come due, `AssumeRole` sessions expire, and
|
|
174
174
|
Lambda event source mappings re-poll. Inside a simulated Lambda, `Date.now()` and `new Date()`
|
|
175
|
-
report simulated time
|
|
175
|
+
report simulated time. A handler's own expiry logic is exercised without a stub in sight.
|
|
176
176
|
|
|
177
177
|
That last point is worth drawing out. `isolated-testing-style` allows a stub for a clock, on the
|
|
178
|
-
grounds that a clock has no rules worth modelling. Against Yulin that exception is
|
|
178
|
+
grounds that a clock has no rules worth modelling. Against Yulin that exception is unnecessary. The
|
|
179
179
|
clock is part of the simulation, and advancing it exercises the real expiry rules of the services
|
|
180
180
|
around it rather than only the code's own arithmetic.
|
|
181
181
|
|
|
182
182
|
`simAws.clock().resume()` switches to tracking the underlying clock, and `simAws.clock().isFrozen`
|
|
183
|
-
reports which mode it is in. Running mode suits a local dev server
|
|
183
|
+
reports which mode it is in. Running mode suits a local dev server. A test that wants it usually
|
|
184
184
|
wants an advance instead.
|
|
185
185
|
|
|
186
186
|
## Assert by reading the simulation back
|
|
187
187
|
|
|
188
|
-
The simulation holds real state
|
|
188
|
+
The simulation holds real state. The assertion can read it. After exercising the code, ask the
|
|
189
189
|
service what happened rather than asking the SDK what it was told:
|
|
190
190
|
|
|
191
191
|
```typescript
|
|
@@ -193,11 +193,11 @@ service what happened rather than asking the SDK what it was told:
|
|
|
193
193
|
const object = await simAws.s3().getObject(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
194
194
|
```
|
|
195
195
|
|
|
196
|
-
Service accessors take the same Command objects the SDK does
|
|
197
|
-
|
|
196
|
+
Service accessors take the same Command objects the SDK does. The seeding and assertion code reads
|
|
197
|
+
like the production code between them.
|
|
198
198
|
|
|
199
199
|
This is where simulation pays off over stubs a second time. A call-count assertion holds only for
|
|
200
|
-
today's implementation
|
|
200
|
+
today's implementation. A state assertion holds however the handler is rewritten, and it fails if
|
|
201
201
|
the call was made in a way the real service would have rejected.
|
|
202
202
|
|
|
203
203
|
## Match service errors by name, not instanceof
|
|
@@ -216,13 +216,13 @@ raising its own classes, and it silently stops matching. Yulin's errors carry th
|
|
|
216
216
|
error names and SDK-shaped `$metadata`, but they are not instances of the SDK classes.
|
|
217
217
|
|
|
218
218
|
This is worth fixing in production code, not worked around in tests. `name` is what the wire
|
|
219
|
-
carries
|
|
219
|
+
carries. The `name` check is the one that is right in both places. A version skew between two
|
|
220
220
|
`@aws-sdk/client-*` packages breaks `instanceof` in production too, just less predictably than the
|
|
221
221
|
simulator does.
|
|
222
222
|
|
|
223
223
|
## Expect refusals, and treat them as a feature
|
|
224
224
|
|
|
225
|
-
Yulin refuses a property it
|
|
225
|
+
Yulin refuses a property it cannot simulate, and never ignores one. That is the right trade.
|
|
226
226
|
silently accepting something that changes real behaviour turns a deploy-time failure into a
|
|
227
227
|
production one.
|
|
228
228
|
|
|
@@ -255,10 +255,10 @@ gap.
|
|
|
255
255
|
|
|
256
256
|
When reporting, the asymmetry matters more than the volume:
|
|
257
257
|
|
|
258
|
-
- A simulator that **stays silent** about something costs
|
|
259
|
-
|
|
258
|
+
- A simulator that **stays silent** about something costs you little. The test leaves that behaviour
|
|
259
|
+
uncovered, where it was already.
|
|
260
260
|
- A simulator that **says 200 where production says 403** converts a deploy-time failure into a
|
|
261
|
-
production one
|
|
261
|
+
production one. That is the opposite of what it is for.
|
|
262
262
|
|
|
263
263
|
So a false pass deserves far more attention than a false refusal. A real example: Yulin authorised a
|
|
264
264
|
Lambda function URL invocation against `lambda:InvokeFunctionUrl` alone. CloudFront origin access
|
|
@@ -296,15 +296,15 @@ it("stores an upload", async () => {
|
|
|
296
296
|
Putting a template deployment in `beforeEach` pays for the whole stack once per test for no
|
|
297
297
|
isolation you did not already have.
|
|
298
298
|
|
|
299
|
-
A template deployment is the only thing usually worth hoisting. Everything else
|
|
300
|
-
seeded row, a bucket key
|
|
299
|
+
A template deployment is the only thing usually worth hoisting. Everything else (the `SimSdk`, a
|
|
300
|
+
seeded row, a bucket key) is cheap enough to build inside the test that needs it, and it is also
|
|
301
301
|
where it is easiest to read. A `beforeEach` that assembles state for tests that do not all want the
|
|
302
302
|
same state is the beginning of the harness this skill opens by arguing against.
|
|
303
303
|
|
|
304
304
|
## Run the handler as a real simulated Lambda
|
|
305
305
|
|
|
306
306
|
Yulin can run an in-process handler as a function in the simulation, rather than calling it
|
|
307
|
-
directly. Its SDK calls are routed into the simulation as the execution role
|
|
307
|
+
directly. Its SDK calls are routed into the simulation as the execution role. The IAM policies in
|
|
308
308
|
the template are exercised too.
|
|
309
309
|
|
|
310
310
|
Bind a handler to a template function at deploy time with `bindings`:
|
|
@@ -316,7 +316,7 @@ await simAws.cloudFormation().deployTemplateFile({
|
|
|
316
316
|
});
|
|
317
317
|
```
|
|
318
318
|
|
|
319
|
-
The handler still runs in process
|
|
319
|
+
The handler still runs in process. It can close over test state and be stepped through in a
|
|
320
320
|
debugger. The difference from calling it directly is that a missing `s3:PutObject` on the execution
|
|
321
321
|
role now fails the test, at the point AWS would have failed it. A binding can target a function by
|
|
322
322
|
`logicalId`, `functionName`, `arn`, `cdkPath`, or `imageRepository` for a container image function.
|