@kensio/yulin-aws-simulation 1.16.0 → 1.18.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.16.0",
4
+ "version": "1.18.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
@@ -38,10 +38,10 @@ Unzip it into `.agents/skills/` and it is installed.
38
38
 
39
39
  ## What it covers
40
40
 
41
- **Deploy your real synthesized template.** Deploy the JSON CDK produced, with
42
- `deployTemplateFile({ templatePath, stackName })`. A construct change that breaks the system breaks
43
- the test. Do not hand-roll a wrapper that reads the file and calls `deployTemplate`: the file path
44
- is how Yulin finds the cloud assembly beside it. A wrapper loses staged CDK assets. `transform`
41
+ **Deploy your real synthesized templates.** Deploy the JSON CDK produced, one template or several,
42
+ with `deployTemplateFile({ templatePath, stackName })`. A construct change that breaks the system
43
+ breaks the test. Do not hand-roll a wrapper that reads the file and calls `deployTemplate`: the file
44
+ path is how Yulin finds the cloud assembly beside it. A wrapper loses staged CDK assets. `transform`
45
45
  handles what a simulation cannot resolve, such as an ARN carrying a real account or a hosted zone ID
46
46
  from a CDK lookup, and `watch` re-applies the file on change for dev servers. `deployCdkOut` deploys
47
47
  a whole cloud assembly, each Stack into the region its own environment names.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kensio/yulin-aws-simulation",
3
- "version": "1.16.0",
3
+ "version": "1.18.0",
4
4
  "description": "How to test AWS code well with the @kensio/yulin in-process simulator.",
5
5
  "keywords": [
6
6
  "agent-skills",
@@ -1,19 +1,25 @@
1
1
  ---
2
2
  name: yulin-aws-simulation
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, deploying a whole cdk.out cloud assembly, intercepting SDK clients with SimSdk, driving HTTP requests into the simulation with SimAwsHttp, 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 a CloudFront Distribution, its DNS records or its certificate need testing, when test setup around Yulin is growing helper classes or wrapper functions, and when Yulin refuses a template property or an SDK command.
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 the templates a CDK app already synthesizes, deploying one of them or a whole cdk.out cloud assembly, binding real handlers to the functions a template declares, intercepting SDK clients with SimSdk, driving HTTP requests into the simulation with SimAwsHttp, controlling simulated time, reading back the names CloudFormation generates, 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 a CloudFront Distribution, its DNS records or its certificate need 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.16.0"
6
+ version: "1.18.0"
7
7
  ---
8
8
 
9
9
  # Testing with Yulin
10
10
 
11
11
  [Yulin](https://yulinsim.dev/) (`@kensio/yulin`) simulates AWS in process, in memory, with no
12
- network and no AWS account. This skill is how to use it well. For the API read
13
- [yulinsim.dev/llms.txt](https://yulinsim.dev/llms.txt), one markdown page per guide and per
14
- simulated service (drop the `llms.txt` for HTML, or read `docs/` in the repository). It serves
15
- `isolated-testing-style`, the general argument for simulation over stubs. Each rule says what it
16
- buys, and a case that does not want that trade can go the other way knowingly.
12
+ network and no AWS account. This skill is how to use it well. It serves `isolated-testing-style`,
13
+ the general argument for simulation over stubs. Each rule says what it buys, and a case that does
14
+ not want that trade can go the other way knowingly.
15
+
16
+ For the API read `node_modules/@kensio/yulin/llms.txt`. It indexes the 45 markdown pages beside it
17
+ under `node_modules/@kensio/yulin/docs/`, one per simulated service and per feature guide,
18
+ documenting the version installed. Open the page it names for the service in hand and grep it for
19
+ the operation or property, since Cognito runs to 210 KB with DynamoDB and Lambda close behind. Where
20
+ the package is absent, or predates the 1.20.x that started carrying it,
21
+ [yulinsim.dev/llms.txt](https://yulinsim.dev/llms.txt) has the same index for the current release
22
+ (drop the `llms.txt` for HTML).
17
23
 
18
24
  ## Use what Yulin already gives you
19
25
 
@@ -26,15 +32,16 @@ side effects, no network, no cleanup and no awaiting. Service accessors take the
26
32
  objects the SDK does. `using simSdk = new SimSdk()` is the teardown, `deployTemplateFile` is the
27
33
  environment, and a simulation per test is close to free.
28
34
 
29
- So a test constructs, deploys if a template is involved, intercepts, exercises, then asserts by
30
- reading the simulation back. A wrapper around any of those steps hides the one thing the reader
31
- needs to see. A repeated sequence can be a small function in the same file returning the simulation
32
- objects themselves. Once it wants a class, an options interface or a directory, it has become a
33
- second product.
35
+ A test constructs, deploys if a template is involved, intercepts, exercises, then reads the
36
+ simulation back. A wrapper around any of those steps hides what the reader needs to see. A repeated
37
+ sequence can be a small function in the same file returning the simulation objects themselves. Once
38
+ it wants a class, an options interface or a directory, it has become a second product.
34
39
 
35
- ## One synthesized template, for tests, local dev and production
40
+ ## One CDK app behind the tests, local dev and production
36
41
 
37
- Describe the infrastructure once in CDK and let the synthesized output drive all three:
42
+ Describe the infrastructure once in CDK and let the synthesized output drive all three. Deploy the
43
+ template files `cdk synth` wrote, one of them or all of them, and talk to the simulation the way
44
+ production talks to AWS.
38
45
 
39
46
  ```typescript
40
47
  const stack = await simAws.region("eu-west-2").cloudFormation().deployTemplateFile({
@@ -45,10 +52,9 @@ const stack = await simAws.region("eu-west-2").cloudFormation().deployTemplateFi
45
52
  await stack.waitForDeployComplete();
46
53
  ```
47
54
 
48
- A test written against a hand-written template only tests the hand-written template, and a dev
49
- environment building its own buckets is a third description whose drift shows up as a bug
50
- reproducing in exactly one of the three places. So infrastructure belongs in the CDK app even when
51
- only a test needs it.
55
+ A hand-written template only tests itself, and a dev environment building its own buckets is a third
56
+ description whose drift shows up as a bug reproducing in one place of the three. Infrastructure
57
+ belongs in the CDK app even when only a test needs it.
52
58
 
53
59
  **Do not hand-roll a wrapper that reads the file and calls `deployTemplate`.** `deployTemplateFile`
54
60
  locates the cloud assembly beside the file, and that is how staged CDK assets resolve. A wrapper
@@ -70,14 +76,32 @@ await simAws.cloudFormation().deployTemplateFile({
70
76
  });
71
77
  ```
72
78
 
73
- Together they retire the derived template file a dev server used to write into `cdk.out` so that it
74
- had something to watch. The watched file is the one CDK wrote, and the adaptation re-applies on
75
- every read.
79
+ The watched file is the one CDK wrote, and the adaptation re-applies on every read.
76
80
 
77
81
  `stack.output("SiteBucketName")` answers a resolved Output narrowed to a string, throwing on one the
78
- template never declared. Do not hand-roll that reader. And note that a failed `cdk synth` leaves the
79
- previous template in `cdk.out` with the tests still passing against it, so check the synthesized
80
- JSON changed before concluding anything from a construct change.
82
+ template never declared. Do not hand-roll that reader. A failed `cdk synth` leaves the previous
83
+ template in `cdk.out` with the tests still passing against it, so check the synthesized JSON changed
84
+ before concluding anything from a construct change.
85
+
86
+ ### Read a generated name back, never write one out
87
+
88
+ A Resource whose template leaves its name out is named `<stack name>-<logical ID>-<tail>`, as an
89
+ account names one. The tail is twelve lowercase hex characters derived from the other two parts,
90
+ standing in for the twelve random ones real CloudFormation appends. Where the two parts overrun the
91
+ service's limit, thirteen characters of it go to the tail and its hyphen and the rest is shared
92
+ between them (a 64 character limit leaves 25 each). The trimmed stack name is what an IAM policy
93
+ scoped by resource prefix matches, and a deploy Role allowed an action on `MyVeryLongStackName-*` is
94
+ refused here as an account refuses it.
95
+
96
+ The same template under the same stack name generates the same name every time. That is the one
97
+ property tempting a test to write the name out. Read it back instead, from a `Ref`, from an
98
+ `Fn::GetAtt` attribute or from the accessor of the service holding it.
99
+ [Names CloudFormation generates](https://yulinsim.dev/services/cloudformation/#names-cloudformation-generates)
100
+ covers the tail and the trimming.
101
+
102
+ An ECS container binding has no way round this. The family CloudFormation generates for an unnamed
103
+ task definition ends in a tail only a deployment reveals. Name the task definition Resource with the
104
+ `logicalId` binding form.
81
105
 
82
106
  ### Register what the app looks up, deploy what the app creates
83
107
 
@@ -87,12 +111,11 @@ simulated resource carrying it comes from. Yulin stands one up at a chosen id wi
87
111
  `simAws.acm().registerCertificate({ arn, domainName })`,
88
112
  `simAws.cognitoIdentityProvider().registerUserPool({ id, name })` and `registerUserPoolClient`.
89
113
 
90
- A registration creates a resource in place of the app creating one, and that is what decides when to
91
- use it. It suits `HostedZone.fromLookup` or a certificate issued by hand outside the app. A resource
92
- some stack in the same app creates wants deploying, since a registration would mean configuring it
93
- by hand and taking its configuration from somewhere other than the deployed template. So register
94
- what the app looks up, deploy what the app creates, and substitute in a `transform` only where a
95
- deployed resource cannot be given the id its template names.
114
+ A registration creates a resource in place of the app creating one. It suits `HostedZone.fromLookup`
115
+ or a certificate issued by hand outside the app. A resource some stack in the same app creates wants
116
+ deploying instead, since a registration takes its configuration from somewhere other than the
117
+ deployed template. Substitute in a `transform` only where a deployed resource cannot be given the id
118
+ its template names.
96
119
 
97
120
  Route 53 needs least of this. An `AWS::Route53::RecordSet` naming a hosted zone id no zone holds
98
121
  gets one registered under that id as the record is created, taking its name from the records naming
@@ -122,9 +145,9 @@ const stacks = await simAws.cloudFormation().deployCdkOut({
122
145
  `stackNames` picks part of an assembly, which most apps need, since most also synthesize a
123
146
  deployment pipeline. `stackOptions` carries the `bindings`, `parameters` and `transform` that
124
147
  `deployTemplateFile` takes for one template, keyed the same way. Its transform is handed the Stacks
125
- the same call has already deployed. A Stack consuming a sibling's value therefore stays inside one
126
- call. Two Stacks passing a plain string between them declare no dependency for the manifest to
127
- carry, and the order they are named in is what puts the value there in time.
148
+ the same call already deployed, so a Stack consuming a sibling's value stays inside one call. Two
149
+ Stacks passing a plain string between them declare no dependency for the manifest to carry, and
150
+ their order in `stackNames` is what puts the value there in time.
128
151
 
129
152
  ## Intercept real SDK clients, never hand-roll stubs
130
153
 
@@ -160,14 +183,10 @@ reachable as `simSdk.simAws`, and `new SimSdk({ simAws })` shares an existing on
160
183
 
161
184
  A fake S3 client stubbing `send` with canned `ListObjectsV2` pages, asserted on through the
162
185
  continuation tokens it recorded, passes for code that built its command without a `Bucket`.
163
- Simulated S3 does the pagination for real. `Prefix`, `MaxKeys`, `ContinuationToken` and `StartAfter`
164
- all apply, `IsTruncated` and `NextContinuationToken` come back as the service sends them, and
186
+ Simulated S3 paginates for real, down to the ETag a real multipart upload produces, and
165
187
  `configureMaxKeysPerPage` lowers the page size so that a bucket of two objects makes a caller walk a
166
- continuation. Uploading real parts gives the object the real `<md5-of-the-part-md5s>-<count>` ETag.
167
-
168
- The residue is small. A couple of answers the service never sends (a truncated page naming no
169
- continuation token) can only come from a fake, and a test reaching for one should say so in a
170
- comment.
188
+ continuation. The residue is small, and a test reaching for a fake to get an answer the service
189
+ never sends should say so in a comment.
171
190
 
172
191
  ## Freeze the clock and advance it deliberately
173
192
 
@@ -183,13 +202,13 @@ const simAws = new SimAws({
183
202
  await simAws.clock().advanceBy({ minutes: 20 });
184
203
  ```
185
204
 
186
- Time-dependent behaviour then becomes something a test asserts on in microseconds. A good deal of
187
- the simulation keys off that clock. EventBridge rules and Scheduler schedules fire only when time is
188
- advanced past them, DynamoDB items pass their TTL, Secrets Manager deletions come due, `AssumeRole`
189
- sessions expire, Lambda event source mappings re-poll, and inside a simulated Lambda `Date.now()`
190
- and `new Date()` report simulated time. So the clock stub `isolated-testing-style` allows is
191
- unnecessary here, since advancing this one exercises the real expiry rules of the services around it
192
- as well as the code's own arithmetic.
205
+ Time-dependent behaviour then becomes something a test asserts on in microseconds, and a good deal
206
+ of the simulation keys off that clock. EventBridge rules and Scheduler schedules fire only when time
207
+ is advanced past them, DynamoDB items pass their TTL, Secrets Manager deletions come due,
208
+ `AssumeRole` sessions expire, Lambda event source mappings re-poll, and inside a simulated Lambda
209
+ `Date.now()` and `new Date()` report simulated time. The clock stub `isolated-testing-style` allows
210
+ is unnecessary here, since advancing this one exercises the real expiry rules of the services around
211
+ it as well as the code's own arithmetic.
193
212
 
194
213
  `simAws.clock().resume()` tracks the underlying clock and `simAws.clock().isFrozen` reports the
195
214
  mode. Running mode suits a local dev server, and a test usually wants an advance.
@@ -207,11 +226,11 @@ A call-count assertion holds only for today's implementation. A state assertion
207
226
  handler is rewritten, and it fails if the call was made in a way the real service would have
208
227
  rejected.
209
228
 
210
- The accessors sit on more than one scope, with `simAws.region(name)` carrying some of the services
211
- and `simAws.region(name).account()` carrying all of them (`logs()` among the ones only the account
212
- scope has), so look on the other scope before concluding a service is missing. Each also takes a
213
- plain `{ input: { ... } }` in place of a Command object. An assertion can therefore read a service
214
- back without adding an `@aws-sdk/client-*` package the production code has no use for.
229
+ The accessors sit on more than one scope. `simAws.region(name)` carries some of the services and
230
+ `simAws.region(name).account()` carries all of them (`logs()` among the account-only ones), so look
231
+ on the other scope before concluding a service is missing. Each also takes a plain
232
+ `{ input: { ... } }` in place of a Command object. An assertion can then read a service back without
233
+ adding an `@aws-sdk/client-*` package the production code has no use for.
215
234
 
216
235
  ## Drive requests into the simulation
217
236
 
@@ -232,11 +251,11 @@ A hostname simulated Route 53 answers for is requested by its own name, with no
232
251
  `localUrl(...)` adapting (an `https` URL works with no certificate set up for it). That one request
233
252
  resolves the hostname, finds the Distribution its alias records point at, and runs the deployed
234
253
  CloudFront Function at viewer-request. The certificate, the Hosted Zone records, the Distribution's
235
- aliases and the function are covered together. A template assertion over the same stack passes with
236
- every Route 53 record missing.
254
+ aliases and the function are covered together, where a template assertion over the same stack passes
255
+ with every Route 53 record missing.
237
256
 
238
- Reach for `serveSimAws` when the request comes from outside the process, such as a browser, `curl`
239
- or an SDK client pointed at a local endpoint. Both go through the same routing and service code, and
257
+ Reach for `serveSimAws` when the request comes from outside the process (a browser, `curl`, an SDK
258
+ client pointed at a local endpoint). Both go through the same routing and service code, and
240
259
  `SimAwsHttp` leaves parallel test files no port to collide over. See
241
260
  [the serving docs](https://yulinsim.dev/serve/) for the API.
242
261
 
@@ -251,9 +270,9 @@ if (error instanceof Error && error.name === "ResourceNotFoundException") { ...
251
270
  ```
252
271
 
253
272
  The SDK exports exception classes, which invites the `instanceof` check. It holds only while exactly
254
- one copy of the SDK package is in play. Two copies in the module graph, a bundler, or a simulator
255
- raising its own classes, and it silently stops matching. Yulin's errors carry the service's real
256
- error names and SDK-shaped `$metadata` without being instances of the SDK classes. Fix it in
273
+ one copy of the SDK package is in play, and two copies in the module graph, a bundler, or a
274
+ simulator raising its own classes all stop it matching silently. Yulin's errors carry the service's
275
+ real error names and SDK-shaped `$metadata` without being instances of the SDK classes. Fix it in
257
276
  production code, where a version skew between two `@aws-sdk/client-*` packages breaks `instanceof`
258
277
  too. `name` is what the wire carries, and is right in both places.
259
278
 
@@ -277,13 +296,12 @@ workaround has to be maintained in every project that hits the same gap.
277
296
 
278
297
  The asymmetry matters more than the volume. A simulator staying silent about something costs little,
279
298
  leaving that behaviour uncovered where it already was. A simulator saying 200 where production says
280
- 403 turns a deploy-time failure into a production one, the opposite of what it is for. So report a
281
- false pass with what production does and what the simulation did, and a false refusal with the
282
- property and the template that carries it. Raise a gap costing nothing but convenience as well, once
283
- it is forcing structural duplication.
299
+ 403 is the opposite of what it is for. So report a false pass with what production does and what the
300
+ simulation did, and a false refusal with the property and the template that carries it. Raise a gap
301
+ costing nothing but convenience as well, once it is forcing structural duplication.
284
302
 
285
303
  A workaround kept while the issue is open wants a comment naming that issue and a revisit when it
286
- closes. Re-read the claims in your own comments on each upgrade. They are the ones nothing tests.
304
+ closes. Re-read those claims on each upgrade. They are the ones nothing tests.
287
305
 
288
306
  ## Deploy expensive context once per test file
289
307
 
@@ -308,16 +326,18 @@ it("stores an upload", async () => {
308
326
  });
309
327
  ```
310
328
 
311
- A template deployment is the only thing usually worth hoisting, and in `beforeEach` it pays for the
329
+ A template deployment is the only thing usually worth hoisting. In `beforeEach` it pays for the
312
330
  whole stack once per test for no isolation you did not already have. The `SimSdk`, a seeded row and
313
- a bucket key belong inside the test that needs them. A `beforeEach` assembling state for tests that
314
- do not all want the same state is the beginning of the harness this skill opens by arguing against.
331
+ a bucket key belong inside the test that needs them, and a `beforeEach` assembling state for tests
332
+ that do not all want the same state is the harness this skill opens by arguing against.
315
333
 
316
- ## Run the handler as a real simulated Lambda
334
+ ## Bind a handler and run it as a real simulated Lambda
317
335
 
318
- Yulin can run an in-process handler as a function inside the simulation. Bind it to a template
319
- function at deploy time with `bindings`, targeting the function by `logicalId`, `functionName`,
320
- `arn`, `cdkPath` or `imageRepository`:
336
+ `bindings` is how to run your own code inside the simulation, for an `AWS::CloudFront::Function` as
337
+ much as for a Lambda function. Bind an in-process handler at deploy time, targeting a Lambda
338
+ function by `logicalId`, `functionName`, `arn`, `cdkPath` or `imageRepository`, and a CloudFront
339
+ Function by `logicalId`, `functionName` or `arn`. On a CloudFront Function it is what covers source
340
+ `cdk synth` embedded or transformed.
321
341
 
322
342
  ```typescript
323
343
  await simAws.cloudFormation().deployTemplateFile({
@@ -352,10 +372,10 @@ accessors. A recording logger was the last reason standing, and from 1.17.1 a bo
352
372
  is recorded into its log group, read back at `/aws/lambda/<function name>` through
353
373
  `FilterLogEvents`.
354
374
 
355
- The console and the process standard streams are both bridged for the length of an invocation, in
356
- the way `process.env` and `Date` are. A logging library building its own `Console` over those
357
- streams at module scope is recorded too, Powertools' `Logger` included, both its JSON log line and
358
- its EMF metric document.
375
+ The console and the process standard streams are bridged for the length of an invocation, as
376
+ `process.env` and `Date` are. A logging library building its own `Console` over those streams at
377
+ module scope is recorded too, Powertools' `Logger` included, both its JSON log line and its EMF
378
+ metric document.
359
379
 
360
380
  So when a test builds the application's own graph, ask what it cannot get through an invocation.
361
381
  Expect the answer to be nothing.
@@ -364,27 +384,30 @@ Expect the answer to be nothing.
364
384
 
365
385
  A bound handler gets the function's declared environment variables with nothing stubbed.
366
386
  `SimProcessEnvironment` holds a run's variables in an `AsyncLocalStorage` store and resolves
367
- `process.env` to it for the length of the run, with concurrent runs each seeing their own. The one
368
- thing it cannot reach is a read that already happened. A handler module doing
387
+ `process.env` to it for the length of the run, with concurrent runs each seeing their own. What it
388
+ cannot reach is a read that already happened. A handler module doing
369
389
  `const TABLE = process.env.TABLE_NAME` at module scope is evaluated when the test file imports it,
370
390
  long before any run, and captures the host value.
371
391
 
372
392
  So read the environment inside the handler body, memoising there where a warm container should build
373
- its clients once. The substituted `Date` works the same way. A `vi.stubEnv` around a bound handler
374
- is the sign of a handler reading too early. `SimLambdaEnvironmentConflicts` warns about this, but
375
- only where the host value and the declared value differ, and a suite that stubs the right values
376
- stays quiet and never learns.
393
+ its clients once. The substituted `Date` works the same way, and a `vi.stubEnv` around a bound
394
+ handler is the sign of a handler reading too early. `SimLambdaEnvironmentConflicts` warns about
395
+ this, but only where the host value and the declared value differ, and a suite that stubs the right
396
+ values stays quiet and never learns.
377
397
 
378
- ### What a binding buys, and what the zip path buys
398
+ ### Keep the zip path for a case about the artefact
379
399
 
380
- Deploying without `bindings` runs the bundle `cdk synth` produced. `deployTemplateFile` publishes
381
- the cloud assembly's assets into the staging bucket in simulated S3, and the modules are evaluated
382
- as CommonJS in a vm sandbox with its own `process.env`, `Date` and HTTP clients, where the
383
- module-scope problem above never arises. Both paths authorise through the execution role, and the
384
- same policy mutation fails a zip-path test exactly as it fails a bound one.
400
+ Deploying a Lambda function without `bindings` runs the bundle `cdk synth` produced.
401
+ `deployTemplateFile` publishes the cloud assembly's assets into the staging bucket in simulated S3,
402
+ and the modules are evaluated as CommonJS in a vm sandbox with its own `process.env`, `Date` and
403
+ HTTP clients, where the module-scope problem above never arises. Both paths authorise through the
404
+ execution role, and the same policy mutation fails a zip-path test exactly as it fails a bound one.
385
405
 
386
- - **A binding** keeps a breakpoint working and lets the handler close over test state.
387
- - **The zip path** exercises the artefact that deploys, its imports and its bundling included.
406
+ What that buys is the artefact that deploys, its imports and its bundling included, and a case about
407
+ the bundle itself is the case to spend it on. Everywhere else it is the fallback. Every run waits on
408
+ the build, a stale `cdk.out` runs yesterday's handler, and the vm runtime loads CommonJS as the real
409
+ `nodejs` runtimes do, leaving a `NodejsFunction` synthesized with `format: OutputFormat.ESM` refused
410
+ at cold start.
388
411
 
389
412
  ### Outbound HTTP is answered by the simulation
390
413
 
@@ -392,7 +415,7 @@ From 1.16.2, a simulated Lambda's `fetch` and its `node:http` and `node:https` a
392
415
  simulation for every hostname simulated Route 53 resolves, through the same in-process entry point a
393
416
  request arriving on localhost uses. A Cognito user pool domain, an HTTP API and a load balancer are
394
417
  all answered without the test knowing which of them it asked, and everything else reaches the
395
- network as it was addressed. This is what makes an OAuth authorization code exchange testable, since
396
- that exchange lives only at the pool domain's hosted `/oauth2/token` endpoint with no SDK operation
397
- behind it. The same routing lets `CognitoJwtVerifier` fetch a simulated pool's JWKS from inside a
398
- handler with no cache primed.
418
+ network as addressed. That is what makes an OAuth authorization code exchange testable, since it
419
+ lives only at the pool domain's hosted `/oauth2/token` endpoint with no SDK operation behind it, and
420
+ it lets `CognitoJwtVerifier` fetch a simulated pool's JWKS from inside a handler with no cache
421
+ primed.