@kensio/yulin-aws-simulation 1.18.0 → 1.18.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.18.0",
4
+ "version": "1.18.1",
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
@@ -91,9 +91,13 @@ authorised a Lambda function URL invocation against `lambda:InvokeFunctionUrl` a
91
91
  CloudFront origin access control also needs `lambda:InvokeFunction`. The tests passed, the release
92
92
  went out, and the endpoint 403'd in production.
93
93
 
94
- **Deploy expensive context once per test file.** Vitest gives each file its own worker. A stack
95
- deployed in `beforeAll` is already isolated between files. Isolation inside the file comes from
96
- randomised names.
94
+ **Share one simulation across the whole test suite.** Create one `SimAws`, deploy the application
95
+ once and install one `SimSdk` interception layer in Vitest setup. Run the suite in one shared worker
96
+ and module context. Tests use random resource names and identifiers while the simulated AWS state
97
+ stays alive, in the same way they would use an AWS account or a LocalStack container. This should be
98
+ the majority of the suite. Put tests that advance or change the shared simulation clock in a smaller
99
+ isolated group, with a fresh Yulin setup for each test. Per-test and per-file simulations are
100
+ supported for these exceptional cases.
97
101
 
98
102
  **Run the handler as a real simulated Lambda.** Bind an in-process handler to a template function
99
103
  and invoke the function through simulated Lambda. Its SDK calls are then routed into the simulation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kensio/yulin-aws-simulation",
3
- "version": "1.18.0",
3
+ "version": "1.18.1",
4
4
  "description": "How to test AWS code well with the @kensio/yulin in-process simulator.",
5
5
  "keywords": [
6
6
  "agent-skills",
@@ -1,9 +1,9 @@
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 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.
3
+ description: Use the @kensio/yulin in-process AWS simulator well when testing AWS code. Share one simulation and SDK interception layer across tests that leave simulated time alone, and isolate the minority that change the clock. Use Yulin directly, deploy CDK-synthesized templates, bind real handlers, intercept SDK clients, drive HTTP with SimAwsHttp, control time, read generated names, match service errors by name, and handle unsupported properties. Use when writing or reviewing tests that touch AWS, replacing SDK stubs, testing CDK stacks or CloudFront resources, simplifying Yulin test setup, or diagnosing unsupported templates and commands.
4
4
  license: Apache-2.0
5
5
  metadata:
6
- version: "1.18.0"
6
+ version: "1.18.1"
7
7
  ---
8
8
 
9
9
  # Testing with Yulin
@@ -13,6 +13,13 @@ network and no AWS account. This skill is how to use it well. It serves `isolate
13
13
  the general argument for simulation over stubs. Each rule says what it buys, and a case that does
14
14
  not want that trade can go the other way knowingly.
15
15
 
16
+ Treat Yulin as suite infrastructure, in the same way tests treat an AWS account or a LocalStack
17
+ container. Create one simulation, deploy the application once and install SDK interception once in
18
+ the Vitest suite setup. Every test talks to that shared environment. Randomised resource names and
19
+ identifiers keep tests independent while the simulated AWS state remains alive for the whole suite.
20
+ This is the default for tests that leave the simulation clock alone. Put the smaller set of tests
21
+ that change simulated time in an isolated group, with their own Yulin setup.
22
+
16
23
  For the API read `node_modules/@kensio/yulin/llms.txt`. It indexes the 45 markdown pages beside it
17
24
  under `node_modules/@kensio/yulin/docs/`, one per simulated service and per feature guide,
18
25
  documenting the version installed. Open the page it names for the service in hand and grep it for
@@ -29,13 +36,13 @@ service, or a `beforeEach` that reassembles the world.
29
36
 
30
37
  Yulin is built to be used directly. `new SimAws()` and `new SimSdk()` are plain constructors with no
31
38
  side effects, no network, no cleanup and no awaiting. Service accessors take the same Command
32
- objects the SDK does. `using simSdk = new SimSdk()` is the teardown, `deployTemplateFile` is the
33
- environment, and a simulation per test is close to free.
39
+ objects the SDK does. The suite setup creates these objects once. `deployTemplateFile` is the
40
+ environment, and the tests import the shared simulation objects directly.
34
41
 
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.
42
+ The setup constructs, deploys and intercepts. A test exercises the application and reads the shared
43
+ simulation back. A wrapper around any of those steps hides what the reader needs to see. The setup
44
+ can be a small module returning the simulation objects themselves. Once it wants a class, an options
45
+ interface or a directory, it has become a second product.
39
46
 
40
47
  ## One CDK app behind the tests, local dev and production
41
48
 
@@ -52,6 +59,9 @@ const stack = await simAws.region("eu-west-2").cloudFormation().deployTemplateFi
52
59
  await stack.waitForDeployComplete();
53
60
  ```
54
61
 
62
+ The test suite deploys this template once during its shared Yulin setup. Individual tests use the
63
+ deployed resources and do not deploy their own copies.
64
+
55
65
  A hand-written template only tests itself, and a dev environment building its own buckets is a third
56
66
  description whose drift shows up as a bug reproducing in one place of the three. Infrastructure
57
67
  belongs in the CDK app even when only a test needs it.
@@ -155,7 +165,7 @@ their order in `stackNames` is what puts the value there in time.
155
165
  from the simulation, and the code under test uses the SDK exactly as it does in production.
156
166
 
157
167
  ```typescript
158
- using simSdk = new SimSdk();
168
+ const simSdk = new SimSdk({ simAws });
159
169
  simSdk.intercept(SecretsManagerClient); // Every instance, including ones made later.
160
170
  ```
161
171
 
@@ -164,20 +174,22 @@ correctly. A stub has no naming rules and verifies no signatures. A malformed Se
164
174
  or a wrongly computed Cognito `SECRET_HASH` passes it and fails in production.
165
175
 
166
176
  Intercept the class in most cases, since the code under test usually constructs its own clients.
167
- Intercept an instance when a single client should reach the simulation, and when a file's cases each
168
- build their own `SimAws`. A class interception is process-wide (it shadows `send` on the class
169
- prototype) and refuses a second install while the first is live, with
170
- `SimSdkAlreadyInterceptedError`. An instance interception goes when the instance does.
177
+ Install that class interception once in the suite setup. A class interception is process-wide (it
178
+ shadows `send` on the class prototype) and refuses a second install while the first is live, with
179
+ `SimSdkAlreadyInterceptedError`. Repeated interception in `beforeEach` or in each test file is both
180
+ unnecessary and an error when those files share a worker. Intercept an instance only when one
181
+ specific client should reach Yulin.
171
182
 
172
183
  Whichever it is, it has to be the client the code actually calls. A `DynamoDBDocumentClient` built
173
184
  over a `DynamoDBClient` is what the code sends through, and the document client is the one to
174
185
  intercept. Every Command routes to the simulation by default, and an allow list of Command classes
175
186
  narrows that where something else should handle the rest.
176
187
 
177
- `SimSdk` and its interception handles are disposable, so `using` restores every intercepted client
178
- at the end of the scope, leaving nothing for a later test to inherit when the one before it threw.
179
- `simSdk.restoreAll()` and `interception.restore()` do it by hand. Each `SimSdk` owns a `SimAws`,
180
- reachable as `simSdk.simAws`, and `new SimSdk({ simAws })` shares an existing one.
188
+ Keep the suite's `SimSdk` alive for as long as the suite. Worker exit normally removes its
189
+ process-wide patches. Call `simSdk.restoreAll()` if the process will continue doing other work.
190
+ `using simSdk = new SimSdk()` and `interception.restore()` remain useful for a deliberately
191
+ short-lived simulation. Each `SimSdk` owns a `SimAws`, reachable as `simSdk.simAws`, and
192
+ `new SimSdk({ simAws })` shares an existing one.
181
193
 
182
194
  ### A fake accepts any request the simulator would refuse
183
195
 
@@ -213,6 +225,19 @@ it as well as the code's own arithmetic.
213
225
  `simAws.clock().resume()` tracks the underlying clock and `simAws.clock().isFrozen` reports the
214
226
  mode. Running mode suits a local dev server, and a test usually wants an advance.
215
227
 
228
+ ### Give clock-changing tests their own simulation
229
+
230
+ The clock is part of a `SimAws` instance's state. Tests sharing that instance also share its current
231
+ time and whether it is frozen or running. One test advancing or resuming the clock therefore changes
232
+ the environment underneath every other test in that group. Randomised resource names cannot isolate
233
+ this change.
234
+
235
+ Most tests should use the suite simulation without changing its clock. Put tests that advance the
236
+ clock or change its mode in a separate Vitest project or file group. Give each of those tests a
237
+ fresh `SimAws`, deployment and interception layer. Each test can then control time without affecting
238
+ another test. A small group may share one isolated simulation when its cases deliberately follow the
239
+ same timeline.
240
+
216
241
  ## Assert by reading the simulation back
217
242
 
218
243
  The simulation holds real state. After exercising the code, ask the service what happened:
@@ -224,7 +249,8 @@ const object = await simAws.s3().getObject(new GetObjectCommand({ Bucket: bucket
224
249
 
225
250
  A call-count assertion holds only for today's implementation. A state assertion holds however the
226
251
  handler is rewritten, and it fails if the call was made in a way the real service would have
227
- rejected.
252
+ rejected. In the shared suite environment, read the resource named by the test's random identifier.
253
+ Do not list the whole service and assume no other test has left state there.
228
254
 
229
255
  The accessors sit on more than one scope. `simAws.region(name)` carries some of the services and
230
256
  `simAws.region(name).account()` carries all of them (`logs()` among the account-only ones), so look
@@ -303,33 +329,86 @@ costing nothing but convenience as well, once it is forcing structural duplicati
303
329
  A workaround kept while the issue is open wants a comment naming that issue and a revisit when it
304
330
  closes. Re-read those claims on each upgrade. They are the ones nothing tests.
305
331
 
306
- ## Deploy expensive context once per test file
332
+ ## Share one simulation across the whole test suite
333
+
334
+ The recommended lifecycle is one `SimAws`, one deployment and one `SimSdk` interception layer for
335
+ the tests that do not change simulated time. This should be most of the test suite. Do this in
336
+ Vitest setup. Tests should treat the result as a long-running AWS account or LocalStack container
337
+ whose state survives every test and test file.
338
+
339
+ An in-process simulation needs the test files to share a worker and module cache. Set
340
+ `fileParallelism: false` and `isolate: false`, then load one setup module through `setupFiles`:
307
341
 
308
- Vitest gives each test file its own worker, so module-level state is already isolated between files.
309
- Deploy a stack once for the file and let the tests share it. Isolation inside the file comes from
310
- randomised names.
342
+ ```typescript
343
+ // vitest.config.ts
344
+ export default defineConfig({
345
+ test: {
346
+ fileParallelism: false,
347
+ isolate: false,
348
+ setupFiles: ["./test/yulin.setup.ts"],
349
+ },
350
+ });
351
+ ```
352
+
353
+ Keep the one-time work in an imported module. Vitest executes a setup file before each test file,
354
+ even with isolation disabled, but imported modules stay cached in the shared worker.
311
355
 
312
356
  ```typescript
313
- let simAws: SimAws;
357
+ // test/yulin-suite.ts
358
+ const ready = (async () => {
359
+ const simAws = new SimAws();
360
+ const simSdk = new SimSdk({ simAws });
361
+
362
+ simSdk.intercept(DynamoDBClient);
363
+ simSdk.intercept(S3Client);
314
364
 
315
- beforeAll(async () => {
316
- simAws = new SimAws();
317
365
  const stack = await simAws.cloudFormation().deployTemplateFile({
318
366
  templatePath: "cdk.out/SiteStack.template.json",
319
367
  });
320
368
  await stack.waitForDeployComplete();
321
- });
322
369
 
323
- it("stores an upload", async () => {
324
- // Given a key no other test in this file is using.
325
- const key = `uploads/${faker.string.uuid()}.png`;
326
- });
370
+ return { simAws, simSdk, stack };
371
+ })();
372
+
373
+ export function yulinSuite() {
374
+ return ready;
375
+ }
327
376
  ```
328
377
 
329
- A template deployment is the only thing usually worth hoisting. In `beforeEach` it pays for the
330
- whole stack once per test for no isolation you did not already have. The `SimSdk`, a seeded row and
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.
378
+ ```typescript
379
+ // test/yulin.setup.ts
380
+ import { yulinSuite } from "./yulin-suite";
381
+
382
+ await yulinSuite();
383
+ ```
384
+
385
+ Tests import `yulinSuite()` and work against that same state. Give every test data a random name or
386
+ identifier, query by that identifier and never assume a service starts empty. A missing-resource
387
+ case uses a new identifier that no test created. A count assertion records the relevant count before
388
+ the action when it cannot query by identifier. Tests remain independent without clearing tables,
389
+ buckets or queues between them.
390
+
391
+ ```typescript
392
+ const { simAws } = await yulinSuite();
393
+ const key = `uploads/${faker.string.uuid()}.png`;
394
+
395
+ // Exercise the application, then read this test's object from shared S3 state.
396
+ const object = await simAws.s3().getObject({ input: { Bucket: bucket, Key: key } });
397
+ ```
398
+
399
+ Do not put the simulation in Vitest `globalSetup`. That hook runs outside the test workers and can
400
+ only pass serializable values into them. `setupFiles` runs in the test worker and can share the live
401
+ `SimAws` object through the cached module above.
402
+
403
+ This shared setup also shares the simulation clock. Keep tests that call `advanceBy`, resume the
404
+ clock or otherwise change simulated time out of this group. Put them in a separate Vitest project or
405
+ file pattern, such as `*.clock.iso.test.ts`, and give each test an isolated Yulin setup. The split
406
+ keeps the common suite setup cheap while allowing the smaller clock-testing group to control time.
407
+
408
+ Creating Yulin once per test or once per file is supported. Reserve it for a case that deliberately
409
+ tests an entire fresh account, an independent clock or incompatible interception. Use the shared
410
+ setup by default. A `beforeEach` or file-level `beforeAll` that deploys the application again
411
+ usually turns random test data into repeated infrastructure work.
333
412
 
334
413
  ## Bind a handler and run it as a real simulated Lambda
335
414