@kensio/yulin-aws-simulation 1.13.0 → 1.14.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.
@@ -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.13.0",
4
+ "version": "1.14.0",
5
5
  "description": "How to test AWS code well with the @kensio/yulin in-process simulator.",
6
6
  "author": {
7
7
  "name": "Kensio Software",
package/README.md CHANGED
@@ -76,8 +76,10 @@ deployed in `beforeAll` is already isolated between files. Isolation inside the
76
76
  randomised names.
77
77
 
78
78
  **Run the handler as a real simulated Lambda.** Bind an in-process handler to a template function
79
- and its SDK calls are routed into the simulation as the execution role. A missing permission on that
80
- role fails the test at the point AWS would have failed it.
79
+ and invoke the function through simulated Lambda. Its SDK calls are then routed into the simulation
80
+ as the execution role, and a missing permission on that role fails the test at the point AWS would
81
+ have failed it. Calling the bound handler directly skips all of that, and so does reading
82
+ `process.env` at module scope.
81
83
 
82
84
  ## Related skills
83
85
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kensio/yulin-aws-simulation",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "description": "How to test AWS code well with the @kensio/yulin in-process simulator.",
5
5
  "keywords": [
6
6
  "agent-skills",
@@ -3,7 +3,7 @@ name: yulin-aws-simulation
3
3
  description: Use the @kensio/yulin in-process AWS simulator well when testing AWS code, using it directly rather than building a harness around it, driving tests, local dev and production from one synthesized CDK template, intercepting SDK clients with SimSdk, controlling simulated time, matching service errors by name, and handling properties Yulin refuses to simulate. Use when writing or reviewing tests that touch AWS, when replacing aws-sdk-client-mock or hand-rolled AWS stubs, when a CDK stack needs testing, when test setup around Yulin is growing helper classes or wrapper functions, and when Yulin refuses a template property or an SDK command.
4
4
  license: Apache-2.0
5
5
  metadata:
6
- version: "1.13.0"
6
+ version: "1.14.0"
7
7
  ---
8
8
 
9
9
  # Testing with Yulin
@@ -12,9 +12,11 @@ metadata:
12
12
  network and no AWS account. Its own docs are the authority on the API. This skill covers how to use
13
13
  it well, and that lives mostly outside the API.
14
14
 
15
- Read the package docs for anything API-shaped. Start with
16
- [the README](https://github.com/KensioSoftware/yulin#readme), then `docs/sdk/` for interception and
17
- `docs/services/<name>/` for each simulated service.
15
+ Read the package docs for anything API-shaped.
16
+ [yulinsim.dev/llms.txt](https://yulinsim.dev/llms.txt) indexes every page as plain markdown, one
17
+ link per guide and one per simulated service. Drop the `llms.txt` from a link for the HTML page. The
18
+ same pages ship in the repository, as [the README](https://github.com/KensioSoftware/yulin#readme),
19
+ then `docs/sdk/` for interception and `docs/services/<name>/` for each simulated service.
18
20
 
19
21
  This skill serves the `isolated-testing-style` skill, the general argument for simulation over
20
22
  stubs.
@@ -84,8 +86,7 @@ The two options that make a wrapper unnecessary:
84
86
 
85
87
  - **`transform`** is given the parsed template and answers with the one to deploy. It runs on the
86
88
  deployment and again on every re-read. A wrapper cannot do that. Use it for what a simulation
87
- genuinely cannot resolve, such as an ARN carrying a real account, or a hosted zone ID that came
88
- from `HostedZone.fromLookup`.
89
+ genuinely cannot resolve, such as an ARN carrying a real account.
89
90
  - **`watch`** re-applies the file when it changes, updating the stack in place. This is for dev
90
91
  servers. A `cdk synth` becomes a stack update without restarting the process, and resources the
91
92
  change left alone keep what they hold.
@@ -93,11 +94,23 @@ The two options that make a wrapper unnecessary:
93
94
  ```typescript
94
95
  await simAws.cloudFormation().deployTemplateFile({
95
96
  templatePath: "cdk.out/SiteStack.template.json",
96
- transform: withoutLookedUpHostedZone,
97
+ transform: withRealAccountArnsResolved,
97
98
  watch: { onUpdated: () => srv.reload() },
98
99
  });
99
100
  ```
100
101
 
102
+ A hosted zone ID that came from `HostedZone.fromLookup` needs no transform of its own. An
103
+ `AWS::Route53::RecordSet` naming a hosted zone ID that no zone holds gets one registered under that
104
+ ID as the record is created, and its records resolve. The zone takes its name from the records that
105
+ name it. Register it yourself with `simAws.route53().registerHostedZone({ id, name })` where a test
106
+ depends on the name, such as one listing zones by name.
107
+
108
+ A failed `cdk synth` leaves the previous template in `cdk.out` where it was, and the tests carry on
109
+ running against it. An edit that leaves an unused variable behind fails `tsc` and exits non-zero,
110
+ with the error hidden behind a redirect. The test that runs next passes against the template from
111
+ before the change. Check that the synthesized JSON changed before concluding anything from a
112
+ construct change.
113
+
101
114
  ## Intercept real SDK clients, never hand-roll stubs
102
115
 
103
116
  `SimSdk` replaces the `send` method of an AWS SDK client class or instance, so real clients answer
@@ -270,6 +283,11 @@ control also needs `lambda:InvokeFunction`. The tests passed, the release went o
270
283
  Report a false pass with what production does and what the simulation did. Report a false refusal
271
284
  with the property and the template that carries it.
272
285
 
286
+ A workaround kept while the issue is open wants a comment naming that issue, and a revisit when it
287
+ closes. One test file carried a comment saying its handler could not reach simulated AWS from inside
288
+ a simulated Lambda, citing KensioSoftware/yulin#638. That held when it was written and was fixed in
289
+ 1.16.0. The comment went on shaping the design of the test for months after that.
290
+
273
291
  ## Deploy expensive context once per test file
274
292
 
275
293
  Vitest gives each test file its own worker, so module-level state is already isolated between files.
@@ -322,3 +340,74 @@ The handler still runs in process. It can close over test state and be stepped t
322
340
  debugger. The difference from calling it directly is that a missing `s3:PutObject` on the execution
323
341
  role now fails the test, at the point AWS would have failed it. A binding can target a function by
324
342
  `logicalId`, `functionName`, `arn`, `cdkPath`, or `imageRepository` for a container image function.
343
+
344
+ ### Invoke through simulated Lambda
345
+
346
+ Binding a handler proves nothing about IAM on its own. The execution role, the function's
347
+ environment and its outbound HTTP are all applied by the invocation. The test has to go through
348
+ `simAws.lambda().invoke(new InvokeCommand({ FunctionName, Payload }))` to get any of them. A test
349
+ holding the same handler reference and calling it directly runs it in the test's own scope, as the
350
+ test's own caller, with none of the three.
351
+
352
+ The difference shows up under mutation. Removing `dynamodb:GetItem` from the role's policy in the
353
+ CDK stack and re-synthesizing failed every invoked case:
354
+
355
+ ```
356
+ AccessDenied: User: arn:aws:iam::111111111111:role/UserFunctionServiceRole1B2C3D4E
357
+ is not authorized to perform: dynamodb:GetItem
358
+ ```
359
+
360
+ The cases calling the handler directly passed. A suite that stays green through that mutation was
361
+ never covering the policy.
362
+
363
+ ### Read the environment inside the handler
364
+
365
+ A bound handler gets the function's declared environment variables with nothing stubbed.
366
+ `SimProcessEnvironment` holds a run's variables in an `AsyncLocalStorage` store and resolves
367
+ `process.env` to that store for the length of the run. Concurrent runs each see their own. Its own
368
+ doc comment states the one limit:
369
+
370
+ <!-- prose-check:off -->
371
+
372
+ > The one thing this cannot reach is a read that already happened. A handler module doing
373
+ > `const TABLE = process.env.TABLE_NAME` at module scope is evaluated when the test file imports it,
374
+ > long before any run, so it captures the host value.
375
+
376
+ <!-- prose-check:on -->
377
+
378
+ So read the environment inside the handler body. Memoise there where a warm container should build
379
+ its clients once. The substituted `Date` a bound handler reads works the same way, and a time
380
+ captured at module scope is captured equally early.
381
+
382
+ Yulin ships `SimLambdaEnvironmentConflicts` to warn about this, and it warns only where the host
383
+ value and the declared value differ. A suite that stubs the right values stays quiet and never
384
+ learns. The rule stays invisible for as long as the stubs agree.
385
+
386
+ A `vi.stubEnv` around a bound handler is the sign that the handler reads too early. Moving the reads
387
+ into the handler body removed every `vi.stubEnv` from one repository.
388
+
389
+ ### What a binding buys, and what the zip path buys
390
+
391
+ Deploying without `bindings` runs the bundle `cdk synth` produced. `deployTemplateFile` publishes
392
+ the cloud assembly's assets into the staging bucket in simulated S3, and the function's modules are
393
+ evaluated as CommonJS in a vm sandbox. That sandbox hands the code its own `process.env`, its own
394
+ `Date` and its own HTTP clients, and the module-scope problem above never arises there.
395
+
396
+ Both paths authorise through the execution role. The same policy mutation fails a zip-path test
397
+ exactly as it fails a bound one. The choice between them is about what else the test needs:
398
+
399
+ - **A binding** keeps a breakpoint working and lets the handler close over test state.
400
+ - **The zip path** exercises the artefact that deploys, its imports and its bundling included.
401
+
402
+ ### Outbound HTTP is answered by the simulation
403
+
404
+ From 1.16.2, a simulated Lambda's `fetch` and its `node:http` and `node:https` are answered by the
405
+ simulation for every hostname simulated Route 53 resolves. Those requests go through the same
406
+ in-process entry point one arriving on localhost uses. A Cognito user pool domain, an HTTP API and a
407
+ load balancer are all answered without the test knowing which of them it asked. Everything else
408
+ reaches the network as it was addressed.
409
+
410
+ This is what makes an OAuth authorization code exchange testable. That exchange lives only at the
411
+ hosted `/oauth2/token` endpoint of the user pool domain and has no SDK operation behind it. A
412
+ handler that cannot reach the domain leaves the whole exchange uncovered. The same routing lets
413
+ `CognitoJwtVerifier` fetch a simulated pool's JWKS from inside a handler with no cache primed.