@kensio/yulin-aws-simulation 1.3.0 → 1.6.1

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.
@@ -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.3.0",
4
+ "version": "1.6.1",
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 that is not in the
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 })`, so a construct change that breaks the system
28
- breaks the test. Do not hand-roll a wrapper that reads the file and calls `deployTemplate`: the file
29
- path is how Yulin finds the cloud assembly beside it, so a wrapper loses staged CDK assets.
30
- `transform` handles what a simulation cannot resolve, such as an ARN carrying a real account or a
31
- hosted zone ID from a CDK lookup, and `watch` re-applies the file on change for dev servers.
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, so it
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 does not simulate
47
- rather than ignoring it, because silently accepting something that changes real behaviour is worse.
48
- The cost is that one unsupported setting can make a whole stack unsimulatable, so enumerate every
49
- refusal in one pass: strip properties from the synthesized template until it deploys, then raise
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 nothing. One that says 200 where production says 403 converts a
54
- deploy-time failure into a production one, which is the opposite of what it is for. Yulin once
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, so a stack
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, so a missing permission on
65
- that role fails the test at the point AWS would have failed it.
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.0",
3
+ "version": "1.6.1",
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, which is mostly not in the API.
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, which is the general argument for simulation
17
- over stubs.
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 rather than a set of rules: each one says what it buys, so a situation that
21
- does not want that trade can go the other way knowingly.
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 a private framework wrapped around a
28
- tool that is already the framework.
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, nothing to
33
- undo, nothing to await. Creating a simulation per test costs approximately nothing.
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: construct, deploy if a template is involved, intercept,
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 rather than a bespoke shape of its own. The point at
45
- which it wants a class, an options interface, or a directory, it has stopped being test setup and
46
- become a second product.
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: what runs locally is what CI tested
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, so the assets manifest and
76
- staged asset directories resolve. A wrapper that reads the JSON itself loses that, and anything
77
- needing a CDK asset, such as a `Custom::CDKBucketDeployment` or a `Code.fromAsset` function, fails
78
- with `No CDK assets manifest is available.`
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, which is what a wrapper cannot do. Use it for what a
84
- simulation genuinely cannot resolve: an ARN carrying a real account, or a hosted zone ID that came
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: a `cdk synth` becomes a stack update without restarting the process, and resources the
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, which is exactly the
117
- suffix 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, so it had accepted it happily.
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 rather than the instance in most cases, since the code under test usually
123
- constructs its own clients. Intercept an instance when only one client should reach the simulation.
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, so it has to be given the client the code
131
- under test actually calls. The wrapper clients are where this bites: a `DynamoDBDocumentClient`
132
- built over a `DynamoDBClient` is what the code sends through, so it is the document client that
133
- needs intercepting, not the client underneath it.
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, which is worth reaching for only when something else should genuinely
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, so a handler's own expiry logic is exercised without a stub in sight.
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 not needed: the
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; a test that wants it usually
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, so the assertion can read it. After exercising the code, ask the
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, so the seeding and assertion code
197
- reads like the production code between them.
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; a state assertion holds however the handler is rewritten, and it fails if
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, so the `name` check is the one that is right in both places. A version skew between two
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 does not simulate rather than ignoring it. That is the right trade:
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 nothing. The test does not cover that
259
- behaviour, which is where it was already.
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, which is the opposite of what it is for.
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 the `SimSdk`, a
300
- seeded row, a bucket key is cheap enough to build inside the test that needs it, which is also
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, so the IAM policies in
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, so it can close over test state and be stepped through in a
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.